Reworking encryption/decryption API.

This commit is contained in:
Paul Schaub 2021-05-06 00:04:03 +02:00
parent 7e2c89b1b3
commit 89a0adddd8
29 changed files with 1454 additions and 631 deletions

View File

@ -11,7 +11,7 @@ About
PGPainless aims to make using OpenPGP in Java projects as simple as possible.
It does so by introducing an intuitive Builder structure, which allows easy
setup of encryption / decrytion operations, as well as straight forward key generation.
setup of encryptionOptions / decrytion operations, as well as straight forward key generation.
PGPainless is based around the Bouncycastle java library and can be used on Android down to API level 10.
@ -74,7 +74,7 @@ Take for example a look at this delicious key:
### Encrypt / Sign Data
Encrypting and signing data is pretty straight forward as well.
Encrypting and signingOptions data is pretty straight forward as well.
```java
EncryptionStream encryptor = PGPainless.encryptAndOrSign()
.onOutputStream(targetOuputStream)
@ -102,7 +102,7 @@ Additionally you can get information about the encrypted data by calling
OpenPgpMetadata result = encryptor.getResult();
```
This object will contain information like to which keys the message is encrypted, which keys were used for signing and so on.
This object will contain information like to which keys the message is encrypted, which keys were used for signingOptions and so on.
### Decrypt / Verify Encrypted Data

View File

@ -0,0 +1,34 @@
/*
* Copyright 2021 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pgpainless.algorithm;
public enum DocumentSignatureType {
BINARY_DOCUMENT(SignatureType.BINARY_DOCUMENT),
CANONICAL_TEXT_DOCUMENT(SignatureType.CANONICAL_TEXT_DOCUMENT),
;
final SignatureType signatureType;
DocumentSignatureType(SignatureType signatureType) {
this.signatureType = signatureType;
}
public SignatureType getSignatureType() {
return signatureType;
}
}

View File

@ -17,311 +17,202 @@ package org.pgpainless.encryption_signing;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.Nonnull;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.CompressionAlgorithm;
import org.pgpainless.algorithm.DocumentSignatureType;
import org.pgpainless.algorithm.HashAlgorithm;
import org.pgpainless.algorithm.KeyFlag;
import org.pgpainless.algorithm.SignatureType;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.decryption_verification.OpenPgpMetadata;
import org.pgpainless.key.KeyRingValidator;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.exception.KeyValidationException;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.key.protection.UnlockSecretKey;
import org.pgpainless.policy.Policy;
import org.pgpainless.util.Passphrase;
import org.pgpainless.util.Tuple;
import org.pgpainless.util.selection.key.PublicKeySelectionStrategy;
import org.pgpainless.util.selection.key.SecretKeySelectionStrategy;
import org.pgpainless.util.selection.key.impl.And;
import org.pgpainless.util.selection.key.impl.EncryptionKeySelectionStrategy;
import org.pgpainless.util.selection.key.impl.NoRevocation;
import org.pgpainless.util.selection.key.impl.SignatureKeySelectionStrategy;
public class EncryptionBuilder implements EncryptionBuilderInterface {
private final EncryptionStream.Purpose purpose;
private OutputStream outputStream;
private final Map<SubkeyIdentifier, PGPPublicKeyRing> encryptionKeys = new ConcurrentHashMap<>();
private final Set<Passphrase> encryptionPassphrases = new HashSet<>();
private boolean detachedSignature = false;
private SignatureType signatureType = SignatureType.BINARY_DOCUMENT;
private final Map<SubkeyIdentifier, PGPSecretKeyRing> signingKeys = new ConcurrentHashMap<>();
private SecretKeyRingProtector signingKeysDecryptor;
private SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm.AES_128;
private HashAlgorithm hashAlgorithm = HashAlgorithm.SHA256;
private CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.UNCOMPRESSED;
private boolean asciiArmor = false;
private EncryptionOptions encryptionOptions;
private SigningOptions signingOptions = new SigningOptions();
private ProducerOptions options;
private OpenPgpMetadata.FileInfo fileInfo;
public EncryptionBuilder() {
this.purpose = EncryptionStream.Purpose.COMMUNICATIONS;
this.encryptionOptions = new EncryptionOptions(EncryptionStream.Purpose.COMMUNICATIONS);
}
public EncryptionBuilder(@Nonnull EncryptionStream.Purpose purpose) {
this.purpose = purpose;
this.encryptionOptions = new EncryptionOptions(purpose);
}
@Override
public ToRecipients onOutputStream(@Nonnull OutputStream outputStream, OpenPgpMetadata.FileInfo fileInfo) {
public ToRecipientsOrNoEncryption onOutputStream(@Nonnull OutputStream outputStream, OpenPgpMetadata.FileInfo fileInfo) {
this.outputStream = outputStream;
this.fileInfo = fileInfo;
return new ToRecipientsImpl();
return new ToRecipientsOrNoEncryptionImpl();
}
class ToRecipientsImpl implements ToRecipients {
@Override
public WithAlgorithms toRecipients(@Nonnull PGPPublicKeyRing... keys) {
if (keys.length == 0) {
throw new IllegalArgumentException("No public keys provided.");
}
public AdditionalRecipients toRecipient(@Nonnull PGPPublicKeyRing key) {
encryptionOptions.addRecipient(key);
return new AdditionalRecipientsImpl();
}
Map<SubkeyIdentifier, PGPPublicKeyRing> encryptionKeys = new ConcurrentHashMap<>();
@Override
public AdditionalRecipients toRecipient(@Nonnull PGPPublicKeyRing key, @Nonnull String userId) {
encryptionOptions.addRecipient(key, userId);
return new AdditionalRecipientsImpl();
}
@Override
public AdditionalRecipients toRecipient(@Nonnull PGPPublicKeyRingCollection keys, @Nonnull String userId) {
for (PGPPublicKeyRing ring : keys) {
PGPPublicKeyRing validatedKeyRing = KeyRingValidator.validate(ring, PGPainless.getPolicy());
for (PGPPublicKey k : validatedKeyRing) {
if (encryptionKeySelector().accept(k)) {
encryptionKeys.put(new SubkeyIdentifier(ring, k.getKeyID()), ring);
}
}
encryptionOptions.addRecipient(ring, userId);
}
if (encryptionKeys.isEmpty()) {
throw new IllegalArgumentException("No valid encryption keys found!");
}
EncryptionBuilder.this.encryptionKeys.putAll(encryptionKeys);
return new WithAlgorithmsImpl();
}
private String getPrimaryUserId(PGPPublicKey publicKey) {
// TODO: Use real function to get primary userId.
return publicKey.getUserIDs().next();
return new AdditionalRecipientsImpl();
}
@Override
public WithAlgorithms toRecipients(@Nonnull PGPPublicKeyRingCollection... keys) {
if (keys.length == 0) {
throw new IllegalArgumentException("No key ring collections provided.");
public AdditionalRecipients toRecipients(@Nonnull PGPPublicKeyRingCollection keys) {
for (PGPPublicKeyRing ring : keys) {
encryptionOptions.addRecipient(ring);
}
for (PGPPublicKeyRingCollection collection : keys) {
for (PGPPublicKeyRing ring : collection) {
Map<SubkeyIdentifier, PGPPublicKeyRing> encryptionKeys = new ConcurrentHashMap<>();
for (PGPPublicKey k : ring) {
if (encryptionKeySelector().accept(k)) {
encryptionKeys.put(new SubkeyIdentifier(ring, k.getKeyID()), ring);
}
}
if (encryptionKeys.isEmpty()) {
throw new IllegalArgumentException("No valid encryption keys found!");
}
EncryptionBuilder.this.encryptionKeys.putAll(encryptionKeys);
}
}
return new WithAlgorithmsImpl();
return new AdditionalRecipientsImpl();
}
@Override
public WithAlgorithms forPassphrases(Passphrase... passphrases) {
List<Passphrase> passphraseList = new ArrayList<>();
for (Passphrase passphrase : passphrases) {
if (passphrase.isEmpty()) {
throw new IllegalArgumentException("Passphrase must not be empty.");
}
passphraseList.add(passphrase);
}
EncryptionBuilder.this.encryptionPassphrases.addAll(passphraseList);
return new WithAlgorithmsImpl();
}
@Override
public DetachedSign doNotEncrypt() {
return new DetachedSignImpl();
public AdditionalRecipients forPassphrase(Passphrase passphrase) {
encryptionOptions.addPassphrase(passphrase);
return new AdditionalRecipientsImpl();
}
}
class WithAlgorithmsImpl implements WithAlgorithms {
class ToRecipientsOrNoEncryptionImpl extends ToRecipientsImpl implements ToRecipientsOrNoEncryption {
@Override
public WithAlgorithms andToSelf(@Nonnull PGPPublicKeyRing... keys) {
if (keys.length == 0) {
throw new IllegalArgumentException("Recipient list MUST NOT be empty.");
public EncryptionStream withOptions(ProducerOptions options) throws PGPException, IOException {
if (options == null) {
throw new NullPointerException("ProducerOptions cannot be null.");
}
for (PGPPublicKeyRing ring : keys) {
Map<SubkeyIdentifier, PGPPublicKeyRing> encryptionKeys = new ConcurrentHashMap<>();
for (Iterator<PGPPublicKey> i = ring.getPublicKeys(); i.hasNext(); ) {
PGPPublicKey key = i.next();
if (encryptionKeySelector().accept(key)) {
encryptionKeys.put(new SubkeyIdentifier(ring, key.getKeyID()), ring);
}
}
if (encryptionKeys.isEmpty()) {
throw new IllegalArgumentException("No suitable encryption key found in the key ring " + new OpenPgpV4Fingerprint(ring));
}
EncryptionBuilder.this.encryptionKeys.putAll(encryptionKeys);
}
return this;
return new EncryptionStream(outputStream, options, fileInfo);
}
@Override
public WithAlgorithms andToSelf(@Nonnull PGPPublicKeyRingCollection keys) {
for (PGPPublicKeyRing ring : keys) {
Map<SubkeyIdentifier, PGPPublicKeyRing> encryptionKeys = new ConcurrentHashMap<>();
for (Iterator<PGPPublicKey> i = ring.getPublicKeys(); i.hasNext(); ) {
PGPPublicKey key = i.next();
if (encryptionKeySelector().accept(key)) {
encryptionKeys.put(new SubkeyIdentifier(ring, key.getKeyID()), ring);
}
}
if (encryptionKeys.isEmpty()) {
throw new IllegalArgumentException("No suitable encryption key found in the key ring " + new OpenPgpV4Fingerprint(ring));
}
EncryptionBuilder.this.encryptionKeys.putAll(encryptionKeys);
}
return this;
}
@Override
public DetachedSign usingAlgorithms(@Nonnull SymmetricKeyAlgorithm symmetricKeyAlgorithm,
@Nonnull HashAlgorithm hashAlgorithm,
@Nonnull CompressionAlgorithm compressionAlgorithm) {
EncryptionBuilder.this.symmetricKeyAlgorithm = symmetricKeyAlgorithm;
EncryptionBuilder.this.hashAlgorithm = hashAlgorithm;
EncryptionBuilder.this.compressionAlgorithm = compressionAlgorithm;
return new DetachedSignImpl();
}
@Override
public DetachedSign usingSecureAlgorithms() {
EncryptionBuilder.this.symmetricKeyAlgorithm = SymmetricKeyAlgorithm.AES_256;
EncryptionBuilder.this.hashAlgorithm = HashAlgorithm.SHA512;
EncryptionBuilder.this.compressionAlgorithm = CompressionAlgorithm.UNCOMPRESSED;
return new DetachedSignImpl();
}
@Override
public ToRecipients and() {
return new ToRecipientsImpl();
public SignWithOrDontSign doNotEncrypt() {
EncryptionBuilder.this.encryptionOptions = null;
return new SignWithOrDontSignImpl();
}
}
class DetachedSignImpl implements DetachedSign {
class AdditionalRecipientsImpl implements AdditionalRecipients {
@Override
public ToRecipientsOrSign and() {
return new ToRecipientsOrSignImpl();
}
}
class ToRecipientsOrSignImpl extends ToRecipientsImpl implements ToRecipientsOrSign {
@Override
public SignWith createDetachedSignature() {
EncryptionBuilder.this.detachedSignature = true;
return new SignWithImpl();
public Armor doNotSign() {
EncryptionBuilder.this.signingOptions = null;
return new ArmorImpl();
}
@Override
public AdditionalSignWith signWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRing... keyRings) throws KeyValidationException {
return new SignWithImpl().signWith(decryptor, keyRings);
}
@Override
public AdditionalSignWith signWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRingCollection keyRings) {
return new SignWithImpl().signWith(decryptor, keyRings);
}
@Override
public AdditionalSignWith signInlineWith(@Nonnull SecretKeyRingProtector secretKeyDecryptor, @Nonnull PGPSecretKeyRing signingKey, String userId, DocumentSignatureType signatureType) throws PGPException {
return new SignWithImpl().signInlineWith(secretKeyDecryptor, signingKey, userId, signatureType);
}
@Override
public AdditionalSignWith signDetachedWith(@Nonnull SecretKeyRingProtector secretKeyDecryptor, @Nonnull PGPSecretKeyRing signingKey, String userId, DocumentSignatureType signatureType) throws PGPException {
return new SignWithImpl().signDetachedWith(secretKeyDecryptor, signingKey, userId, signatureType);
}
}
class SignWithOrDontSignImpl extends SignWithImpl implements SignWithOrDontSign {
@Override
public Armor doNotSign() {
return new ArmorImpl();
}
@Override
public DocumentType signWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRing... keyRings) {
return new SignWithImpl().signWith(decryptor, keyRings);
}
@Override
public DocumentType signWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRingCollection keyRings) {
return new SignWithImpl().signWith(decryptor, keyRings);
}
}
class SignWithImpl implements SignWith {
@Override
public DocumentType signWith(@Nonnull SecretKeyRingProtector decryptor,
@Nonnull PGPSecretKeyRing... keyRings) {
if (keyRings.length == 0) {
throw new IllegalArgumentException("Signing key list MUST NOT be empty.");
public AdditionalSignWith signWith(@Nonnull SecretKeyRingProtector decryptor,
@Nonnull PGPSecretKeyRing... keyRings)
throws KeyValidationException {
for (PGPSecretKeyRing secretKeyRing : keyRings) {
signingOptions.addInlineSignature(decryptor, secretKeyRing, DocumentSignatureType.BINARY_DOCUMENT);
}
for (PGPSecretKeyRing ring : keyRings) {
Map<SubkeyIdentifier, PGPSecretKeyRing> signingKeys = new ConcurrentHashMap<>();
for (Iterator<PGPSecretKey> i = ring.getSecretKeys(); i.hasNext(); ) {
PGPSecretKey s = i.next();
if (EncryptionBuilder.this.signingKeySelector().accept(s)) {
signingKeys.put(new SubkeyIdentifier(ring, s.getKeyID()), ring);
}
}
if (signingKeys.isEmpty()) {
throw new IllegalArgumentException("No suitable signing key found in the key ring " + new OpenPgpV4Fingerprint(ring));
}
EncryptionBuilder.this.signingKeys.putAll(signingKeys);
}
EncryptionBuilder.this.signingKeysDecryptor = decryptor;
return new DocumentTypeImpl();
return new AdditionalSignWithImpl();
}
@Override
public DocumentType signWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRingCollection keyRings) {
Iterator<PGPSecretKeyRing> iterator = keyRings.iterator();
if (!iterator.hasNext()) {
throw new IllegalArgumentException("Signing key collection MUST NOT be empty.");
public AdditionalSignWith signWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRingCollection keyRings)
throws KeyValidationException {
for (PGPSecretKeyRing key : keyRings) {
signingOptions.addInlineSignature(decryptor, key, DocumentSignatureType.BINARY_DOCUMENT);
}
while (iterator.hasNext()) {
PGPSecretKeyRing ring = iterator.next();
Map<SubkeyIdentifier, PGPSecretKeyRing> signingKeys = new ConcurrentHashMap<>();
for (Iterator<PGPSecretKey> i = ring.getSecretKeys(); i.hasNext(); ) {
PGPSecretKey s = i.next();
if (EncryptionBuilder.this.signingKeySelector().accept(s)) {
signingKeys.put(new SubkeyIdentifier(ring, s.getKeyID()), ring);
}
}
return new AdditionalSignWithImpl();
}
if (signingKeys.isEmpty()) {
throw new IllegalArgumentException("No suitable signing key found in the key ring " + new OpenPgpV4Fingerprint(ring));
}
@Override
public AdditionalSignWith signInlineWith(@Nonnull SecretKeyRingProtector secretKeyDecryptor,
@Nonnull PGPSecretKeyRing signingKey,
String userId,
DocumentSignatureType signatureType)
throws KeyValidationException, PGPException {
signingOptions.addInlineSignature(secretKeyDecryptor, signingKey, userId, signatureType);
return new AdditionalSignWithImpl();
}
EncryptionBuilder.this.signingKeys.putAll(signingKeys);
}
EncryptionBuilder.this.signingKeysDecryptor = decryptor;
return new DocumentTypeImpl();
@Override
public AdditionalSignWith signDetachedWith(@Nonnull SecretKeyRingProtector secretKeyDecryptor,
@Nonnull PGPSecretKeyRing signingKey,
String userId,
DocumentSignatureType signatureType)
throws PGPException, KeyValidationException {
signingOptions.addInlineSignature(secretKeyDecryptor, signingKey, userId, signatureType);
return new AdditionalSignWithImpl();
}
}
class DocumentTypeImpl implements DocumentType {
class AdditionalSignWithImpl implements AdditionalSignWith {
@Override
public Armor signBinaryDocument() {
EncryptionBuilder.this.signatureType = SignatureType.BINARY_DOCUMENT;
return new ArmorImpl();
public SignWith and() {
return new SignWithImpl();
}
@Override
public Armor signCanonicalText() {
EncryptionBuilder.this.signatureType = SignatureType.CANONICAL_TEXT_DOCUMENT;
return new ArmorImpl();
public EncryptionStream asciiArmor() throws IOException, PGPException {
return new ArmorImpl().asciiArmor();
}
@Override
public EncryptionStream noArmor() throws IOException, PGPException {
return new ArmorImpl().noArmor();
}
}
@ -329,70 +220,87 @@ public class EncryptionBuilder implements EncryptionBuilderInterface {
@Override
public EncryptionStream asciiArmor() throws IOException, PGPException {
EncryptionBuilder.this.asciiArmor = true;
assignProducerOptions();
options.setAsciiArmor(true);
return build();
}
@Override
public EncryptionStream noArmor() throws IOException, PGPException {
EncryptionBuilder.this.asciiArmor = false;
assignProducerOptions();
options.setAsciiArmor(false);
return build();
}
private EncryptionStream build() throws IOException, PGPException {
Map<SubkeyIdentifier, Tuple<PGPSecretKeyRing, PGPPrivateKey>> privateKeys = new ConcurrentHashMap<>();
for (SubkeyIdentifier signingKey : signingKeys.keySet()) {
PGPSecretKeyRing secretKeyRing = signingKeys.get(signingKey);
PGPSecretKey secretKey = secretKeyRing.getSecretKey(signingKey.getSubkeyFingerprint().getKeyId());
PBESecretKeyDecryptor decryptor = signingKeysDecryptor.getDecryptor(secretKey.getKeyID());
PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, decryptor);
privateKeys.put(signingKey, new Tuple<>(secretKeyRing, privateKey));
}
return new EncryptionStream(
EncryptionBuilder.this.outputStream,
EncryptionBuilder.this.encryptionKeys,
EncryptionBuilder.this.encryptionPassphrases,
EncryptionBuilder.this.detachedSignature,
signatureType,
privateKeys,
EncryptionBuilder.this.symmetricKeyAlgorithm,
EncryptionBuilder.this.hashAlgorithm,
EncryptionBuilder.this.compressionAlgorithm,
EncryptionBuilder.this.asciiArmor,
EncryptionBuilder.this.options,
fileInfo);
}
}
PublicKeySelectionStrategy encryptionKeySelector() {
KeyFlag[] flags = mapPurposeToKeyFlags(purpose);
return new And.PubKeySelectionStrategy(
new NoRevocation.PubKeySelectionStrategy(),
new EncryptionKeySelectionStrategy(flags));
}
SecretKeySelectionStrategy signingKeySelector() {
return new And.SecKeySelectionStrategy(
new NoRevocation.SecKeySelectionStrategy(),
new SignatureKeySelectionStrategy());
}
private static KeyFlag[] mapPurposeToKeyFlags(EncryptionStream.Purpose purpose) {
KeyFlag[] flags;
switch (purpose) {
case COMMUNICATIONS:
flags = new KeyFlag[] {KeyFlag.ENCRYPT_COMMS};
break;
case STORAGE:
flags = new KeyFlag[] {KeyFlag.ENCRYPT_STORAGE};
break;
case STORAGE_AND_COMMUNICATIONS:
flags = new KeyFlag[] {KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE};
break;
default:
throw new AssertionError("Illegal purpose enum value encountered.");
private void assignProducerOptions() {
if (encryptionOptions != null && signingOptions != null) {
options = ProducerOptions.signAndEncrypt(encryptionOptions, signingOptions);
} else if (encryptionOptions != null) {
options = ProducerOptions.encrypt(encryptionOptions);
} else if (signingOptions != null) {
options = ProducerOptions.sign(signingOptions);
} else {
options = ProducerOptions.noEncryptionNoSigning();
}
}
return flags;
}
/**
* Negotiate the {@link SymmetricKeyAlgorithm} used for message encryption.
* If the user chose to set an override ({@link EncryptionOptions#overrideEncryptionAlgorithm(SymmetricKeyAlgorithm)}, use that.
* Otherwise find an algorithm which is acceptable for all recipients.
* If no consensus can be reached, use {@link Policy.SymmetricKeyAlgorithmPolicy#getDefaultSymmetricKeyAlgorithm()}.
*
* @param encryptionOptions encryption options
* @return negotiated symmetric key algorithm
*/
public static SymmetricKeyAlgorithm negotiateSymmetricEncryptionAlgorithm(EncryptionOptions encryptionOptions) {
SymmetricKeyAlgorithm encryptionAlgorithmOverride = encryptionOptions.getEncryptionAlgorithmOverride();
if (encryptionAlgorithmOverride != null) {
return encryptionAlgorithmOverride;
}
// TODO: Negotiation
return PGPainless.getPolicy().getSymmetricKeyAlgorithmPolicy().getDefaultSymmetricKeyAlgorithm();
}
/**
* Negotiate the {@link HashAlgorithm} used for signatures.
*
* If we encrypt and sign, we look at the recipients keys to determine which algorithm to use.
* If we only sign, we look at the singing keys preferences instead.
*
* @param encryptionOptions encryption options (recipients keys)
* @param signingOptions signing options (signing keys)
* @return negotiated hash algorithm
*/
public static HashAlgorithm negotiateSignatureHashAlgorithm(EncryptionOptions encryptionOptions, SigningOptions signingOptions) {
HashAlgorithm hashAlgorithmOverride = signingOptions.getHashAlgorithmOverride();
if (hashAlgorithmOverride != null) {
return hashAlgorithmOverride;
}
// TODO: Negotiation
return PGPainless.getPolicy().getSignatureHashAlgorithmPolicy().defaultHashAlgorithm();
}
public static CompressionAlgorithm negotiateCompressionAlgorithm(ProducerOptions producerOptions) {
CompressionAlgorithm compressionAlgorithmOverride = producerOptions.getCompressionAlgorithmOverride();
if (compressionAlgorithmOverride != null) {
return compressionAlgorithmOverride;
}
// TODO: Negotiation
return PGPainless.getPolicy().getCompressionAlgorithmPolicy().defaultCompressionAlgorithm();
}
}

View File

@ -25,11 +25,10 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.pgpainless.algorithm.CompressionAlgorithm;
import org.pgpainless.algorithm.HashAlgorithm;
import org.pgpainless.algorithm.DocumentSignatureType;
import org.pgpainless.algorithm.StreamEncoding;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.decryption_verification.OpenPgpMetadata;
import org.pgpainless.exception.KeyValidationException;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.util.Passphrase;
@ -42,7 +41,7 @@ public interface EncryptionBuilderInterface {
* @param outputStream output stream of the plain data.
* @return api handle
*/
default ToRecipients onOutputStream(@Nonnull OutputStream outputStream) {
default ToRecipientsOrNoEncryption onOutputStream(@Nonnull OutputStream outputStream) {
return onOutputStream(outputStream, OpenPgpMetadata.FileInfo.binaryStream());
}
/**
@ -55,7 +54,7 @@ public interface EncryptionBuilderInterface {
*
* @deprecated use {@link #onOutputStream(OutputStream, OpenPgpMetadata.FileInfo)} instead.
*/
default ToRecipients onOutputStream(@Nonnull OutputStream outputStream, boolean forYourEyesOnly) {
default ToRecipientsOrNoEncryption onOutputStream(@Nonnull OutputStream outputStream, boolean forYourEyesOnly) {
return onOutputStream(outputStream, forYourEyesOnly ? OpenPgpMetadata.FileInfo.forYourEyesOnly() : OpenPgpMetadata.FileInfo.binaryStream());
}
@ -70,7 +69,7 @@ public interface EncryptionBuilderInterface {
*
* @deprecated use {@link #onOutputStream(OutputStream, OpenPgpMetadata.FileInfo)} instead.
*/
default ToRecipients onOutputStream(@Nonnull OutputStream outputStream, String fileName, boolean forYourEyesOnly) {
default ToRecipientsOrNoEncryption onOutputStream(@Nonnull OutputStream outputStream, String fileName, boolean forYourEyesOnly) {
return onOutputStream(outputStream, new OpenPgpMetadata.FileInfo(forYourEyesOnly ? "_CONSOLE" : fileName, new Date(), StreamEncoding.BINARY));
}
@ -82,102 +81,99 @@ public interface EncryptionBuilderInterface {
* @param fileInfo file information
* @return api handle
*/
ToRecipients onOutputStream(@Nonnull OutputStream outputStream, OpenPgpMetadata.FileInfo fileInfo);
ToRecipientsOrNoEncryption onOutputStream(@Nonnull OutputStream outputStream, OpenPgpMetadata.FileInfo fileInfo);
interface ToRecipients {
interface ToRecipientsOrNoEncryption extends ToRecipients {
/**
* Pass in a list of trusted public key rings of the recipients.
* Create an {@link EncryptionStream} with the given options (recipients, signers, algorithms...).
*
* @param keys recipient keys for which the message will be encrypted.
* @return api handle
* @param options options
* @return encryption strea
*/
WithAlgorithms toRecipients(@Nonnull PGPPublicKeyRing... keys);
/**
* Pass in a list of trusted public key ring collections of the recipients.
*
* @param keys recipient keys for which the message will be encrypted.
* @return api handle
*/
WithAlgorithms toRecipients(@Nonnull PGPPublicKeyRingCollection... keys);
/**
* Encrypt to one or more symmetric passphrases.
* Note that the passphrases MUST NOT be empty.
*
* @param passphrases passphrase
* @return api handle
*/
WithAlgorithms forPassphrases(Passphrase... passphrases);
EncryptionStream withOptions(ProducerOptions options) throws PGPException, IOException;
/**
* Instruct the {@link EncryptionStream} to not encrypt any data.
*
* @return api handle
*/
DetachedSign doNotEncrypt();
SignWithOrDontSign doNotEncrypt();
}
interface ToRecipients {
/**
* Encrypt for the given valid public key.
* TODO: Explain the difference between this and {@link #toRecipient(PGPPublicKeyRing, String)}.
*
* @param key recipient key for which the message will be encrypted.
* @return api handle
*/
AdditionalRecipients toRecipient(@Nonnull PGPPublicKeyRing key);
/**
* Encrypt for the given valid key using the provided user-id signature to determine preferences.
*
* @param key public key
* @param userId user-id which is used to select the correct encryption parameters based on preferences.
* @return api handle
*/
AdditionalRecipients toRecipient(@Nonnull PGPPublicKeyRing key, @Nonnull String userId);
/**
* Encrypt for the first valid key in the provided keys collection which has a valid user-id that matches
* the provided userId.
* The user-id is also used to determine encryption preferences.
*
* @param keys collection of keys
* @param userId user-id used to select the correct key
* @return api handle
*/
AdditionalRecipients toRecipient(@Nonnull PGPPublicKeyRingCollection keys, @Nonnull String userId);
/**
* Encrypt for all valid public keys in the provided collection.
* If any key is not eligible for encryption (e.g. expired, revoked...), an exception will be thrown.
* TODO: which exception?
*
* @param keys collection of public keys
* @return api handle
*/
AdditionalRecipients toRecipients(@Nonnull PGPPublicKeyRingCollection keys);
/**
* Symmetrically encrypt the message using a passphrase.
* Note that the passphrase MUST NOT be empty.
*
* @param passphrase passphrase
* @return api handle
*/
AdditionalRecipients forPassphrase(Passphrase passphrase);
}
interface WithAlgorithms {
interface AdditionalRecipients {
/**
* Add our own public key to the list of recipient keys.
*
* @param keys own public keys
* @return api handle
*/
WithAlgorithms andToSelf(@Nonnull PGPPublicKeyRing... keys);
/**
* Add our own public keys to the list of recipient keys.
*
* @param keys own public keys
* @return api handle
*/
WithAlgorithms andToSelf(@Nonnull PGPPublicKeyRingCollection keys);
/**
* Specify which algorithms should be used for the encryption.
*
* @param symmetricKeyAlgorithm symmetric algorithm for the session key
* @param hashAlgorithm hash algorithm
* @param compressionAlgorithm compression algorithm
* @return api handle
*/
DetachedSign usingAlgorithms(@Nonnull SymmetricKeyAlgorithm symmetricKeyAlgorithm,
@Nonnull HashAlgorithm hashAlgorithm,
@Nonnull CompressionAlgorithm compressionAlgorithm);
/**
* Use a suite of algorithms that are considered secure.
* Add an additional recipient key/passphrase or configure signing.
*
* @return api handle
*/
DetachedSign usingSecureAlgorithms();
ToRecipients and();
ToRecipientsOrSign and();
}
interface DetachedSign extends SignWith {
/**
* Instruct the {@link EncryptionStream} to generate detached signatures instead of One-Pass-Signatures.
* Those can be retrieved later via {@link OpenPgpMetadata#getSignatures()}.
*
* @return api handle
*/
SignWith createDetachedSignature();
// Allow additional recipient or signing configuration
interface ToRecipientsOrSign extends ToRecipients, SignWithOrDontSign {
}
// Allow signing configuration or no signing at all
interface SignWithOrDontSign extends SignWith {
/**
* Do not sign the plain data at all.
*
* @return api handle
*/
Armor doNotSign();
}
interface SignWith {
@ -186,21 +182,104 @@ public interface EncryptionBuilderInterface {
* Pass in a list of secret keys used for signing, along with a {@link SecretKeyRingProtector} used to unlock
* the secret keys.
*
* @deprecated use {@link #signInlineWith(SecretKeyRingProtector, PGPSecretKeyRing)} instead.
* @param decryptor {@link SecretKeyRingProtector} used to unlock the secret keys
* @param keyRings secret keys used for signing
* @return api handle
*/
DocumentType signWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRing... keyRings);
@Deprecated
AdditionalSignWith signWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRing... keyRings) throws KeyValidationException;
DocumentType signWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRingCollection keyRings);
/**
* Sign inline using the passed in secret keys.
*
* @deprecated use {@link #signInlineWith(SecretKeyRingProtector, PGPSecretKeyRing)} instead.
* @param decryptor for unlocking the secret keys
* @param keyRings secret keys
* @return api handle
*/
@Deprecated
AdditionalSignWith signWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRingCollection keyRings) throws KeyValidationException;
/**
* Create an inline signature using the provided secret key.
* The signature will be of type {@link DocumentSignatureType#BINARY_DOCUMENT}.
*
* @param secretKeyDecryptor for unlocking the secret key
* @param signingKey signing key
* @return api handle
*/
default AdditionalSignWith signInlineWith(@Nonnull SecretKeyRingProtector secretKeyDecryptor, @Nonnull PGPSecretKeyRing signingKey) throws PGPException, KeyValidationException {
return signInlineWith(secretKeyDecryptor, signingKey, null);
}
/**
* Create an inline signature using the provided secret key.
* If userId is not null, the preferences of the matching user-id on the key will be used for signing.
* The signature will be of type {@link DocumentSignatureType#BINARY_DOCUMENT}.
*
* @param secretKeyDecryptor for unlocking the secret key
* @param signingKey signing key
* @param userId userId whose preferences shall be used for signing
* @return api handle
*/
default AdditionalSignWith signInlineWith(@Nonnull SecretKeyRingProtector secretKeyDecryptor, @Nonnull PGPSecretKeyRing signingKey, String userId) throws PGPException, KeyValidationException {
return signInlineWith(secretKeyDecryptor, signingKey, userId, DocumentSignatureType.BINARY_DOCUMENT);
}
/**
* Create an inline signature using the provided secret key with the algorithm preferences of the provided user-id.
*
* @param secretKeyDecryptor for unlocking the secret key
* @param signingKey signing key
* @param userId user-id whose preferences shall be used for signing
* @param signatureType signature type
* @return api handle
*/
AdditionalSignWith signInlineWith(@Nonnull SecretKeyRingProtector secretKeyDecryptor, @Nonnull PGPSecretKeyRing signingKey, String userId, DocumentSignatureType signatureType) throws KeyValidationException, PGPException;
/**
* Create a detached signature using the provided secret key.
*
* @param secretKeyDecryptor for unlocking the secret key
* @param signingKey signing key
* @return api handle
*/
default AdditionalSignWith signDetachedWith(@Nonnull SecretKeyRingProtector secretKeyDecryptor, @Nonnull PGPSecretKeyRing signingKey) throws PGPException, KeyValidationException {
return signDetachedWith(secretKeyDecryptor, signingKey, null);
}
/**
* Create a detached signature using the provided secret key with the algorithm preferences of the provided user-id.
*
* @param secretKeyDecryptor for unlocking the secret key
* @param signingKey signing key
* @param userId user-id whose preferences shall be used for signing
* @return api handle
*/
default AdditionalSignWith signDetachedWith(@Nonnull SecretKeyRingProtector secretKeyDecryptor, @Nonnull PGPSecretKeyRing signingKey, String userId) throws PGPException, KeyValidationException {
return signDetachedWith(secretKeyDecryptor, signingKey, userId, DocumentSignatureType.BINARY_DOCUMENT);
}
/**
* Create a detached signature using the provided secret key with the algorithm preferences of the provided user-id.
*
* @param secretKeyDecryptor for unlocking the secret key
* @param signingKey signing key
* @param userId user-id whose preferences shall be used for signing
* @param signatureType type of the signature
* @return api handle
*/
AdditionalSignWith signDetachedWith(@Nonnull SecretKeyRingProtector secretKeyDecryptor, @Nonnull PGPSecretKeyRing signingKey, String userId, DocumentSignatureType signatureType) throws PGPException, KeyValidationException;
}
interface DocumentType {
Armor signBinaryDocument();
Armor signCanonicalText();
interface AdditionalSignWith extends Armor {
/**
* Add an additional signing key/method.
*
* @return api handle
*/
SignWith and();
}
interface Armor {

View File

@ -0,0 +1,129 @@
/*
* Copyright 2021 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pgpainless.encryption_signing;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator;
import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.implementation.ImplementationFactory;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.key.info.KeyRingInfo;
import org.pgpainless.util.Passphrase;
public class EncryptionOptions {
private final EncryptionStream.Purpose purpose;
private final Set<PGPKeyEncryptionMethodGenerator> encryptionMethods = new LinkedHashSet<>();
private final Set<SubkeyIdentifier> encryptionKeys = new LinkedHashSet<>();
private final Map<SubkeyIdentifier, KeyRingInfo> keyRingInfo = new HashMap<>();
private SymmetricKeyAlgorithm encryptionAlgorithmOverride = null;
public EncryptionOptions(EncryptionStream.Purpose purpose) {
this.purpose = purpose;
}
/**
* Add a recipient by providing a key and recipient user-id.
* The user-id is used to determine the recipients preferences (algorithms etc.).
*
* @param key key ring
* @param userId user id
*/
public void addRecipient(PGPPublicKeyRing key, String userId) {
KeyRingInfo info = new KeyRingInfo(key, new Date());
PGPPublicKey encryptionSubkey = info.getEncryptionSubkey(userId, purpose);
if (encryptionSubkey == null) {
throw new AssertionError("Key has no encryption subkey.");
}
addRecipientKey(key, encryptionSubkey);
}
/**
* Add a recipient by providing a key.
*
* @param key key ring
*/
public void addRecipient(PGPPublicKeyRing key) {
KeyRingInfo info = new KeyRingInfo(key, new Date());
PGPPublicKey encryptionSubkey = info.getEncryptionSubkey(purpose);
if (encryptionSubkey == null) {
throw new AssertionError("Key has no encryption subkey.");
}
addRecipientKey(key, encryptionSubkey);
}
private void addRecipientKey(PGPPublicKeyRing keyRing, PGPPublicKey key) {
encryptionKeys.add(new SubkeyIdentifier(keyRing, key.getKeyID()));
PGPKeyEncryptionMethodGenerator encryptionMethod = ImplementationFactory
.getInstance().getPublicKeyKeyEncryptionMethodGenerator(key);
addEncryptionMethod(encryptionMethod);
}
/**
* Add a symmetric passphrase which the message will be encrypted to.
*
* @param passphrase passphrase
*/
public void addPassphrase(Passphrase passphrase) {
if (passphrase.isEmpty()) {
throw new IllegalArgumentException("Passphrase must not be empty.");
}
PBEKeyEncryptionMethodGenerator encryptionMethod = ImplementationFactory
.getInstance().getPBEKeyEncryptionMethodGenerator(passphrase);
addEncryptionMethod(encryptionMethod);
}
/**
* Add an {@link PGPKeyEncryptionMethodGenerator} which will be used to encrypt the message.
* Method generators are either {@link PBEKeyEncryptionMethodGenerator} (passphrase)
* or {@link PGPKeyEncryptionMethodGenerator} (public key).
*
* This method is intended for advanced users to allow encryption for specific subkeys.
* This can come in handy for example if data needs to be encrypted to a subkey that's ignored by PGPainless.
*
* @param encryptionMethod encryption method
*/
public void addEncryptionMethod(PGPKeyEncryptionMethodGenerator encryptionMethod) {
encryptionMethods.add(encryptionMethod);
}
public Set<PGPKeyEncryptionMethodGenerator> getEncryptionMethods() {
return new HashSet<>(encryptionMethods);
}
public Set<SubkeyIdentifier> getEncryptionKeyIdentifiers() {
return new HashSet<>(encryptionKeys);
}
public SymmetricKeyAlgorithm getEncryptionAlgorithmOverride() {
return encryptionAlgorithmOverride;
}
public void overrideEncryptionAlgorithm(SymmetricKeyAlgorithm encryptionAlgorithm) {
this.encryptionAlgorithmOverride = encryptionAlgorithm;
}
}

View File

@ -0,0 +1,127 @@
/*
* Copyright 2021 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pgpainless.encryption_signing;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.bouncycastle.openpgp.PGPSignature;
import org.pgpainless.algorithm.CompressionAlgorithm;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.decryption_verification.OpenPgpMetadata;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.util.MultiMap;
public final class EncryptionResult {
private final SymmetricKeyAlgorithm encryptionAlgorithm;
private final CompressionAlgorithm compressionAlgorithm;
private final MultiMap<SubkeyIdentifier, PGPSignature> detachedSignatures;
private final Set<SubkeyIdentifier> recipients;
private final OpenPgpMetadata.FileInfo fileInfo;
private EncryptionResult(SymmetricKeyAlgorithm encryptionAlgorithm,
CompressionAlgorithm compressionAlgorithm,
MultiMap<SubkeyIdentifier, PGPSignature> detachedSignatures,
Set<SubkeyIdentifier> recipients,
OpenPgpMetadata.FileInfo fileInfo) {
this.encryptionAlgorithm = encryptionAlgorithm;
this.compressionAlgorithm = compressionAlgorithm;
this.detachedSignatures = detachedSignatures;
this.recipients = Collections.unmodifiableSet(recipients);
this.fileInfo = fileInfo;
}
@Deprecated
public SymmetricKeyAlgorithm getSymmetricKeyAlgorithm() {
return getEncryptionAlgorithm();
}
public SymmetricKeyAlgorithm getEncryptionAlgorithm() {
return encryptionAlgorithm;
}
public CompressionAlgorithm getCompressionAlgorithm() {
return compressionAlgorithm;
}
public MultiMap<SubkeyIdentifier, PGPSignature> getDetachedSignatures() {
return detachedSignatures;
}
public Set<SubkeyIdentifier> getRecipients() {
return recipients;
}
public OpenPgpMetadata.FileInfo getFileInfo() {
return fileInfo;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private SymmetricKeyAlgorithm encryptionAlgorithm;
private CompressionAlgorithm compressionAlgorithm;
private final MultiMap<SubkeyIdentifier, PGPSignature> detachedSignatures = new MultiMap<>();
private Set<SubkeyIdentifier> recipients = new HashSet<>();
private OpenPgpMetadata.FileInfo fileInfo;
public Builder setEncryptionAlgorithm(SymmetricKeyAlgorithm encryptionAlgorithm) {
this.encryptionAlgorithm = encryptionAlgorithm;
return this;
}
public Builder setCompressionAlgorithm(CompressionAlgorithm compressionAlgorithm) {
this.compressionAlgorithm = compressionAlgorithm;
return this;
}
public Builder addRecipient(SubkeyIdentifier recipient) {
this.recipients.add(recipient);
return this;
}
public Builder addDetachedSignature(SubkeyIdentifier signingSubkeyIdentifier, PGPSignature detachedSignature) {
this.detachedSignatures.put(signingSubkeyIdentifier, detachedSignature);
return this;
}
public Builder setFileInfo(OpenPgpMetadata.FileInfo fileInfo) {
this.fileInfo = fileInfo;
return this;
}
public EncryptionResult build() {
if (encryptionAlgorithm == null) {
throw new IllegalStateException("Encryption algorithm not set.");
}
if (compressionAlgorithm == null) {
throw new IllegalStateException("Compression algorithm not set.");
}
if (fileInfo == null) {
throw new IllegalStateException("File info not set.");
}
return new EncryptionResult(encryptionAlgorithm, compressionAlgorithm, detachedSignatures, recipients, fileInfo);
}
}
}

View File

@ -17,10 +17,6 @@ package org.pgpainless.encryption_signing;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nonnull;
@ -31,29 +27,18 @@ import org.bouncycastle.openpgp.PGPCompressedDataGenerator;
import org.bouncycastle.openpgp.PGPEncryptedDataGenerator;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPLiteralDataGenerator;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureGenerator;
import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator;
import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder;
import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder;
import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator;
import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator;
import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder;
import org.pgpainless.algorithm.CompressionAlgorithm;
import org.pgpainless.algorithm.HashAlgorithm;
import org.pgpainless.algorithm.SignatureType;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.decryption_verification.OpenPgpMetadata;
import org.pgpainless.implementation.ImplementationFactory;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.signature.DetachedSignature;
import org.pgpainless.util.ArmoredOutputStreamFactory;
import org.pgpainless.util.Passphrase;
import org.pgpainless.util.Tuple;
/**
* This class is based upon Jens Neuhalfen's Bouncy-GPG PGPEncryptingStream.
@ -81,70 +66,36 @@ public final class EncryptionStream extends OutputStream {
private static final Logger LOGGER = Logger.getLogger(EncryptionStream.class.getName());
private static final Level LEVEL = Level.FINE;
private final ProducerOptions options;
private final EncryptionResult.Builder resultBuilder = EncryptionResult.builder();
private boolean closed = false;
private static final int BUFFER_SIZE = 1 << 8;
private final SymmetricKeyAlgorithm symmetricKeyAlgorithm;
private final HashAlgorithm hashAlgorithm;
private final CompressionAlgorithm compressionAlgorithm;
private final Map<SubkeyIdentifier, PGPPublicKeyRing> encryptionKeys;
private final Set<Passphrase> encryptionPassphrases;
private final boolean detachedSignature;
private final SignatureType signatureType;
private final Map<SubkeyIdentifier, Tuple<PGPSecretKeyRing, PGPPrivateKey>> signingKeys;
private final boolean asciiArmor;
private final OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder();
private Map<SubkeyIdentifier, Tuple<PGPSecretKeyRing, PGPSignatureGenerator>> signatureGenerators = new ConcurrentHashMap<>();
private boolean closed = false;
OutputStream outermostStream = null;
OutputStream outermostStream;
private ArmoredOutputStream armorOutputStream = null;
private OutputStream publicKeyEncryptedStream = null;
private PGPCompressedDataGenerator compressedDataGenerator;
private BCPGOutputStream basicCompressionStream;
private PGPLiteralDataGenerator literalDataGenerator;
private OutputStream literalDataStream;
EncryptionStream(@Nonnull OutputStream targetOutputStream,
@Nonnull Map<SubkeyIdentifier, PGPPublicKeyRing> encryptionKeys,
@Nonnull Set<Passphrase> encryptionPassphrases,
boolean detachedSignature,
SignatureType signatureType,
@Nonnull Map<SubkeyIdentifier, Tuple<PGPSecretKeyRing, PGPPrivateKey>> signingKeys,
@Nonnull SymmetricKeyAlgorithm symmetricKeyAlgorithm,
@Nonnull HashAlgorithm hashAlgorithm,
@Nonnull CompressionAlgorithm compressionAlgorithm,
boolean asciiArmor,
@Nonnull ProducerOptions options,
@Nonnull OpenPgpMetadata.FileInfo fileInfo)
throws IOException, PGPException {
this.symmetricKeyAlgorithm = symmetricKeyAlgorithm;
this.hashAlgorithm = hashAlgorithm;
this.compressionAlgorithm = compressionAlgorithm;
this.encryptionKeys = Collections.unmodifiableMap(encryptionKeys);
this.encryptionPassphrases = Collections.unmodifiableSet(encryptionPassphrases);
this.detachedSignature = detachedSignature;
this.signatureType = signatureType;
this.signingKeys = Collections.unmodifiableMap(signingKeys);
this.asciiArmor = asciiArmor;
this.options = options;
outermostStream = targetOutputStream;
prepareArmor();
prepareEncryption();
prepareSigning();
prepareCompression();
prepareOnePassSignatures();
prepareLiteralDataProcessing(fileInfo);
prepareResultBuilder();
resultBuilder.setFileInfo(fileInfo);
}
private void prepareArmor() {
if (!asciiArmor) {
if (!options.isAsciiArmor()) {
LOGGER.log(LEVEL, "Encryption output will be binary");
return;
}
@ -155,14 +106,18 @@ public final class EncryptionStream extends OutputStream {
}
private void prepareEncryption() throws IOException, PGPException {
if (encryptionKeys.isEmpty() && encryptionPassphrases.isEmpty()) {
EncryptionOptions encryptionOptions = options.getEncryptionOptions();
if (encryptionOptions == null || encryptionOptions.getEncryptionMethods().isEmpty()) {
// No encryption options/methods -> no encryption
resultBuilder.setEncryptionAlgorithm(SymmetricKeyAlgorithm.NULL);
return;
}
LOGGER.log(LEVEL, "At least one encryption key is available -> encrypt using " + symmetricKeyAlgorithm);
SymmetricKeyAlgorithm encryptionAlgorithm = EncryptionBuilder.negotiateSymmetricEncryptionAlgorithm(encryptionOptions);
resultBuilder.setEncryptionAlgorithm(encryptionAlgorithm);
LOGGER.log(LEVEL, "Encrypt message using " + encryptionAlgorithm);
PGPDataEncryptorBuilder dataEncryptorBuilder =
ImplementationFactory.getInstance().getPGPDataEncryptorBuilder(symmetricKeyAlgorithm);
ImplementationFactory.getInstance().getPGPDataEncryptorBuilder(encryptionAlgorithm);
// Simplify once https://github.com/bcgit/bc-java/pull/859 is merged
if (dataEncryptorBuilder instanceof BcPGPDataEncryptorBuilder) {
((BcPGPDataEncryptorBuilder) dataEncryptorBuilder).setWithIntegrityPacket(true);
@ -172,46 +127,21 @@ public final class EncryptionStream extends OutputStream {
PGPEncryptedDataGenerator encryptedDataGenerator =
new PGPEncryptedDataGenerator(dataEncryptorBuilder);
for (SubkeyIdentifier keyIdentifier : encryptionKeys.keySet()) {
LOGGER.log(LEVEL, "Encrypt for key " + keyIdentifier);
PGPPublicKey key = encryptionKeys.get(keyIdentifier).getPublicKey(keyIdentifier.getSubkeyFingerprint().getKeyId());
PublicKeyKeyEncryptionMethodGenerator keyEncryption =
ImplementationFactory.getInstance().getPublicKeyKeyEncryptionMethodGenerator(key);
encryptedDataGenerator.addMethod(keyEncryption);
for (PGPKeyEncryptionMethodGenerator encryptionMethod : encryptionOptions.getEncryptionMethods()) {
encryptedDataGenerator.addMethod(encryptionMethod);
}
for (Passphrase passphrase : encryptionPassphrases) {
PBEKeyEncryptionMethodGenerator passphraseEncryption =
ImplementationFactory.getInstance().getPBEKeyEncryptionMethodGenerator(passphrase);
encryptedDataGenerator.addMethod(passphraseEncryption);
for (SubkeyIdentifier recipientSubkeyIdentifier : encryptionOptions.getEncryptionKeyIdentifiers()) {
resultBuilder.addRecipient(recipientSubkeyIdentifier);
}
publicKeyEncryptedStream = encryptedDataGenerator.open(outermostStream, new byte[BUFFER_SIZE]);
outermostStream = publicKeyEncryptedStream;
}
private void prepareSigning() throws PGPException {
if (signingKeys.isEmpty()) {
return;
}
LOGGER.log(LEVEL, "At least one signing key is available -> sign " + hashAlgorithm + " hash of message");
for (SubkeyIdentifier subkeyIdentifier : signingKeys.keySet()) {
LOGGER.log(LEVEL, "Sign using key " + subkeyIdentifier);
PGPPrivateKey privateKey = signingKeys.get(subkeyIdentifier).getSecond();
PGPContentSignerBuilder contentSignerBuilder = ImplementationFactory.getInstance()
.getPGPContentSignerBuilder(
privateKey.getPublicKeyPacket().getAlgorithm(),
hashAlgorithm.getAlgorithmId());
PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(contentSignerBuilder);
signatureGenerator.init(signatureType.getCode(), privateKey);
signatureGenerators.put(subkeyIdentifier, new Tuple<>(signingKeys.get(subkeyIdentifier).getFirst(), signatureGenerator));
}
}
private void prepareCompression() throws IOException {
CompressionAlgorithm compressionAlgorithm = options.getCompressionAlgorithmOverride();
resultBuilder.setCompressionAlgorithm(compressionAlgorithm);
compressedDataGenerator = new PGPCompressedDataGenerator(
compressionAlgorithm.getAlgorithmId());
if (compressionAlgorithm == CompressionAlgorithm.UNCOMPRESSED) {
@ -224,9 +154,19 @@ public final class EncryptionStream extends OutputStream {
}
private void prepareOnePassSignatures() throws IOException, PGPException {
for (SubkeyIdentifier identifier : signatureGenerators.keySet()) {
PGPSignatureGenerator signatureGenerator = signatureGenerators.get(identifier).getSecond();
signatureGenerator.generateOnePassVersion(false).encode(outermostStream);
SigningOptions signingOptions = options.getSigningOptions();
if (signingOptions == null || signingOptions.getSigningMethods().isEmpty()) {
// No singing options/methods -> no signing
return;
}
for (SubkeyIdentifier identifier : signingOptions.getSigningMethods().keySet()) {
SigningOptions.SigningMethod signingMethod = signingOptions.getSigningMethods().get(identifier);
if (!signingMethod.isDetached()) {
PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator();
signatureGenerator.generateOnePassVersion(false).encode(outermostStream);
}
}
}
@ -238,38 +178,41 @@ public final class EncryptionStream extends OutputStream {
fileInfo.getModificationDate(),
new byte[BUFFER_SIZE]);
outermostStream = literalDataStream;
}
private void prepareResultBuilder() {
for (SubkeyIdentifier recipient : encryptionKeys.keySet()) {
resultBuilder.addRecipientKeyId(recipient.getSubkeyFingerprint().getKeyId());
}
resultBuilder.setSymmetricKeyAlgorithm(symmetricKeyAlgorithm);
resultBuilder.setCompressionAlgorithm(compressionAlgorithm);
resultBuilder.setFileInfo(fileInfo);
}
@Override
public void write(int data) throws IOException {
outermostStream.write(data);
SigningOptions signingOptions = options.getSigningOptions();
if (signingOptions == null || signingOptions.getSigningMethods().isEmpty()) {
return;
}
for (SubkeyIdentifier signingKey : signatureGenerators.keySet()) {
PGPSignatureGenerator signatureGenerator = signatureGenerators.get(signingKey).getSecond();
for (SubkeyIdentifier signingKey : signingOptions.getSigningMethods().keySet()) {
SigningOptions.SigningMethod signingMethod = signingOptions.getSigningMethods().get(signingKey);
PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator();
byte asByte = (byte) (data & 0xff);
signatureGenerator.update(asByte);
}
}
@Override
public void write(byte[] buffer) throws IOException {
public void write(@Nonnull byte[] buffer) throws IOException {
write(buffer, 0, buffer.length);
}
@Override
public void write(byte[] buffer, int off, int len) throws IOException {
public void write(@Nonnull byte[] buffer, int off, int len) throws IOException {
outermostStream.write(buffer, 0, len);
for (SubkeyIdentifier signingKey : signatureGenerators.keySet()) {
PGPSignatureGenerator signatureGenerator = signatureGenerators.get(signingKey).getSecond();
SigningOptions signingOptions = options.getSigningOptions();
if (signingOptions == null || signingOptions.getSigningMethods().isEmpty()) {
return;
}
for (SubkeyIdentifier signingKey : signingOptions.getSigningMethods().keySet()) {
SigningOptions.SigningMethod signingMethod = signingOptions.getSigningMethods().get(signingKey);
PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator();
signatureGenerator.update(buffer, 0, len);
}
}
@ -314,19 +257,23 @@ public final class EncryptionStream extends OutputStream {
}
private void writeSignatures() throws PGPException, IOException {
for (SubkeyIdentifier signingKey : signatureGenerators.keySet()) {
PGPSignatureGenerator signatureGenerator = signatureGenerators.get(signingKey).getSecond();
SigningOptions signingOptions = options.getSigningOptions();
if (signingOptions == null || signingOptions.getSigningMethods().isEmpty()) {
return;
}
for (SubkeyIdentifier signingKey : signingOptions.getSigningMethods().keySet()) {
SigningOptions.SigningMethod signingMethod = signingOptions.getSigningMethods().get(signingKey);
PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator();
PGPSignature signature = signatureGenerator.generate();
if (!detachedSignature) {
if (signingMethod.isDetached()) {
resultBuilder.addDetachedSignature(signingKey, signature);
} else {
signature.encode(outermostStream);
}
DetachedSignature detachedSignature = new DetachedSignature(
signature, signatureGenerators.get(signingKey).getFirst(), signingKey);
resultBuilder.addDetachedSignature(detachedSignature);
}
}
public OpenPgpMetadata getResult() {
public EncryptionResult getResult() {
if (!closed) {
throw new IllegalStateException("EncryptionStream must be closed before accessing the Result.");
}

View File

@ -0,0 +1,135 @@
/*
* Copyright 2021 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pgpainless.encryption_signing;
import javax.annotation.Nullable;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.CompressionAlgorithm;
public final class ProducerOptions {
private final EncryptionOptions encryptionOptions;
private final SigningOptions signingOptions;
private CompressionAlgorithm compressionAlgorithmOverride = PGPainless.getPolicy().getCompressionAlgorithmPolicy()
.defaultCompressionAlgorithm();
private boolean asciiArmor = true;
private ProducerOptions(EncryptionOptions encryptionOptions, SigningOptions signingOptions) {
this.encryptionOptions = encryptionOptions;
this.signingOptions = signingOptions;
}
/**
* Sign and encrypt some data.
*
* @param encryptionOptions encryption options
* @param signingOptions signing options
* @return builder
*/
public static ProducerOptions signAndEncrypt(EncryptionOptions encryptionOptions,
SigningOptions signingOptions) {
throwIfNull(encryptionOptions);
throwIfNull(signingOptions);
return new ProducerOptions(encryptionOptions, signingOptions);
}
/**
* Sign some data without encryption.
*
* @param signingOptions signing options
* @return builder
*/
public static ProducerOptions sign(SigningOptions signingOptions) {
throwIfNull(signingOptions);
return new ProducerOptions(null, signingOptions);
}
/**
* Encrypt some data without signing.
*
* @param encryptionOptions encryption options
* @return builder
*/
public static ProducerOptions encrypt(EncryptionOptions encryptionOptions) {
throwIfNull(encryptionOptions);
return new ProducerOptions(encryptionOptions, null);
}
public static ProducerOptions noEncryptionNoSigning() {
return new ProducerOptions(null, null);
}
private static void throwIfNull(EncryptionOptions encryptionOptions) {
if (encryptionOptions == null) {
throw new NullPointerException("EncryptionOptions cannot be null.");
}
}
private static void throwIfNull(SigningOptions signingOptions) {
if (signingOptions == null) {
throw new NullPointerException("SigningOptions cannot be null.");
}
}
/**
* Override which compression algorithm shall be used.
*
* @param compressionAlgorithm compression algorithm override
* @return builder
*/
public ProducerOptions overrideCompressionAlgorithm(CompressionAlgorithm compressionAlgorithm) {
if (compressionAlgorithm == null) {
throw new NullPointerException("Compression algorithm cannot be null.");
}
this.compressionAlgorithmOverride = compressionAlgorithm;
return this;
}
/**
* Specify, whether or not the result of the encryption/signing operation shall be ascii armored.
* The default value is true.
*
* @param asciiArmor ascii armor
* @return builder
*/
public ProducerOptions setAsciiArmor(boolean asciiArmor) {
this.asciiArmor = asciiArmor;
return this;
}
/**
* Return true if the output of the encryption/signing operation shall be ascii armored.
*
* @return ascii armored
*/
public boolean isAsciiArmor() {
return asciiArmor;
}
public CompressionAlgorithm getCompressionAlgorithmOverride() {
return compressionAlgorithmOverride;
}
public @Nullable EncryptionOptions getEncryptionOptions() {
return encryptionOptions;
}
public @Nullable SigningOptions getSigningOptions() {
return signingOptions;
}
}

View File

@ -0,0 +1,136 @@
/*
* Copyright 2021 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pgpainless.encryption_signing;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignatureGenerator;
import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder;
import org.pgpainless.algorithm.DocumentSignatureType;
import org.pgpainless.algorithm.HashAlgorithm;
import org.pgpainless.exception.KeyValidationException;
import org.pgpainless.implementation.ImplementationFactory;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.key.info.KeyRingInfo;
import org.pgpainless.key.protection.SecretKeyRingProtector;
public final class SigningOptions {
public static final class SigningMethod {
private final PGPSignatureGenerator signatureGenerator;
private final boolean detached;
private SigningMethod(PGPSignatureGenerator signatureGenerator, boolean detached) {
this.signatureGenerator = signatureGenerator;
this.detached = detached;
}
public static SigningMethod inlineSignature(PGPSignatureGenerator signatureGenerator) {
return new SigningMethod(signatureGenerator, false);
}
public static SigningMethod detachedSignature(PGPSignatureGenerator signatureGenerator) {
return new SigningMethod(signatureGenerator, true);
}
public boolean isDetached() {
return detached;
}
public PGPSignatureGenerator getSignatureGenerator() {
return signatureGenerator;
}
}
private Map<SubkeyIdentifier, SigningMethod> signingMethods = new HashMap<>();
private HashAlgorithm hashAlgorithmOverride;
public void addInlineSignature(SecretKeyRingProtector secretKeyDecryptor,
PGPSecretKeyRing secretKey,
DocumentSignatureType signatureType)
throws KeyValidationException {
}
public void addInlineSignature(SecretKeyRingProtector secretKeyDecryptor,
PGPSecretKeyRing secretKey,
String userId,
DocumentSignatureType signatureType)
throws KeyValidationException, PGPException {
KeyRingInfo keyRingInfo = new KeyRingInfo(secretKey, new Date());
if (userId != null) {
if (!keyRingInfo.isUserIdValid(userId)) {
throw new KeyValidationException(userId, keyRingInfo.getCurrentUserIdCertification(userId), keyRingInfo.getUserIdRevocation(userId));
}
}
PGPPublicKey signingPubKey = keyRingInfo.getSigningSubkey();
if (signingPubKey == null) {
throw new AssertionError("Key has no valid signing key.");
}
PGPSecretKey signingSecKey = secretKey.getSecretKey(signingPubKey.getKeyID());
PGPPrivateKey signingSubkey = signingSecKey.extractPrivateKey(secretKeyDecryptor.getDecryptor(signingPubKey.getKeyID()));
List<HashAlgorithm> hashAlgorithms = keyRingInfo.getPreferredHashAlgorithms(userId, signingPubKey.getKeyID());
addSigningMethod(secretKey, signingSubkey, hashAlgorithms.get(0), signatureType, false);
}
private void addSigningMethod(PGPSecretKeyRing secretKey,
PGPPrivateKey signingSubkey,
HashAlgorithm hashAlgorithm,
DocumentSignatureType signatureType,
boolean detached)
throws PGPException {
SubkeyIdentifier signingKeyIdentifier = new SubkeyIdentifier(secretKey, signingSubkey.getKeyID());
PGPSignatureGenerator generator = createSignatureGenerator(signingSubkey, hashAlgorithm, signatureType);
SigningMethod signingMethod = detached ? SigningMethod.detachedSignature(generator) : SigningMethod.inlineSignature(generator);
signingMethods.put(signingKeyIdentifier, signingMethod);
}
private PGPSignatureGenerator createSignatureGenerator(PGPPrivateKey privateKey,
HashAlgorithm hashAlgorithm,
DocumentSignatureType signatureType)
throws PGPException {
int publicKeyAlgorithm = privateKey.getPublicKeyPacket().getAlgorithm();
PGPContentSignerBuilder signerBuilder = ImplementationFactory.getInstance()
.getPGPContentSignerBuilder(publicKeyAlgorithm, hashAlgorithm.getAlgorithmId());
PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(signerBuilder);
signatureGenerator.init(signatureType.getSignatureType().getCode(), privateKey);
return signatureGenerator;
}
public Map<SubkeyIdentifier, SigningMethod> getSigningMethods() {
return Collections.unmodifiableMap(signingMethods);
}
public SigningOptions overrideHashAlgorithm(HashAlgorithm hashAlgorithmOverride) {
this.hashAlgorithmOverride = hashAlgorithmOverride;
return this;
}
public HashAlgorithm getHashAlgorithmOverride() {
return hashAlgorithmOverride;
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright 2021 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pgpainless.exception;
import org.bouncycastle.openpgp.PGPSignature;
public class KeyValidationException extends AssertionError {
public KeyValidationException(String userId, PGPSignature userIdSig, PGPSignature userIdRevocation) {
super("User-ID '" + userId + "' is not valid: Sig: " + userIdSig + " Rev: " + userIdRevocation);
}
}

View File

@ -0,0 +1,36 @@
package org.pgpainless.key;
import java.util.List;
import javax.annotation.Nullable;
import org.bouncycastle.openpgp.PGPSignature;
import org.pgpainless.algorithm.KeyFlag;
import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil;
public interface EvaluatedKeyRing {
PGPSignature getUserIdCertification(String userId);
PGPSignature getUserIdRevocation(String userId);
PGPSignature getSubkeyBinding(long subkeyId);
PGPSignature getSubkeyRevocation(long subkeyId);
default boolean isUserIdRevoked(String userId) {
return getUserIdRevocation(userId) != null;
}
default boolean isSubkeyRevoked(long subkeyId) {
return getSubkeyRevocation(subkeyId) != null;
}
default @Nullable List<KeyFlag> getUserIdKeyFlags(String userId) {
PGPSignature signature = getUserIdCertification(userId);
return SignatureSubpacketsUtil.parseKeyFlags(signature);
}
}

View File

@ -62,6 +62,14 @@ public class SubkeyIdentifier {
this.subkeyFingerprint = subkeyFingerprint;
}
public @Nonnull OpenPgpV4Fingerprint getFingerprint() {
return getSubkeyFingerprint();
}
public long getKeyId() {
return getSubkeyId();
}
/**
* Return the {@link OpenPgpV4Fingerprint} of the primary key of the identified key.
* This might be the same as {@link #getSubkeyFingerprint()} if the identified subkey is the primary key.
@ -72,6 +80,16 @@ public class SubkeyIdentifier {
return primaryKeyFingerprint;
}
/**
* Return the key id of the primary key of the identified key.
* This might be the same as {@link #getSubkeyId()} if the identified subkey is the primary key.
*
* @return primary key id
*/
public long getPrimaryKeyId() {
return getPrimaryKeyFingerprint().getKeyId();
}
/**
* Return the {@link OpenPgpV4Fingerprint} of the identified subkey.
*
@ -81,6 +99,15 @@ public class SubkeyIdentifier {
return subkeyFingerprint;
}
/**
* Return the key id of the identified subkey.
*
* @return subkey id
*/
public long getSubkeyId() {
return getSubkeyFingerprint().getKeyId();
}
@Override
public int hashCode() {
return primaryKeyFingerprint.hashCode() * 31 + subkeyFingerprint.hashCode();

View File

@ -20,12 +20,12 @@ import static org.pgpainless.util.CollectionUtils.iteratorToList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -37,9 +37,14 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.HashAlgorithm;
import org.pgpainless.algorithm.KeyFlag;
import org.pgpainless.algorithm.PublicKeyAlgorithm;
import org.pgpainless.encryption_signing.EncryptionStream;
import org.pgpainless.exception.KeyValidationException;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.policy.Policy;
import org.pgpainless.signature.SignaturePicker;
import org.pgpainless.signature.SignatureUtils;
import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil;
@ -52,13 +57,7 @@ public class KeyRingInfo {
private static final Pattern PATTERN_EMAIL = Pattern.compile("[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}");
private final PGPKeyRing keys;
private final PGPSignature revocationSelfSignature;
private final PGPSignature mostRecentSelfSignature;
private final Map<String, PGPSignature> mostRecentUserIdSignatures = new ConcurrentHashMap<>();
private final Map<String, PGPSignature> mostRecentUserIdRevocations = new ConcurrentHashMap<>();
private final Map<Long, PGPSignature> mostRecentSubkeyBindings = new ConcurrentHashMap<>();
private final Map<Long, PGPSignature> mostRecentSubkeyRevocations = new ConcurrentHashMap<>();
private Signatures signatures;
/**
* Evaluate the key ring at creation time of the given signature.
@ -82,36 +81,7 @@ public class KeyRingInfo {
public KeyRingInfo(PGPKeyRing keys, Date validationDate) {
this.keys = keys;
revocationSelfSignature = SignaturePicker.pickCurrentRevocationSelfSignature(keys, validationDate);
mostRecentSelfSignature = SignaturePicker.pickCurrentDirectKeySelfSignature(keys, validationDate);
for (Iterator<String> it = keys.getPublicKey().getUserIDs(); it.hasNext(); ) {
String userId = it.next();
PGPSignature certification = SignaturePicker.pickCurrentUserIdCertificationSignature(keys, userId, validationDate);
if (certification != null) {
mostRecentUserIdSignatures.put(userId, certification);
}
PGPSignature revocation = SignaturePicker.pickCurrentUserIdRevocationSignature(keys, userId, validationDate);
if (revocation != null) {
mostRecentUserIdRevocations.put(userId, revocation);
}
}
Iterator<PGPPublicKey> publicKeys = keys.getPublicKeys();
publicKeys.next(); // Skip primary key
while (publicKeys.hasNext()) {
PGPPublicKey subkey = publicKeys.next();
PGPSignature bindingSig = SignaturePicker.pickCurrentSubkeyBindingSignature(keys, subkey, validationDate);
if (bindingSig != null) {
mostRecentSubkeyBindings.put(subkey.getKeyID(), bindingSig);
}
PGPSignature bindingRevocation = SignaturePicker.pickCurrentSubkeyBindingRevocationSignature(keys, subkey, validationDate);
if (bindingRevocation != null) {
mostRecentSubkeyRevocations.put(subkey.getKeyID(), bindingRevocation);
}
}
this.signatures = new Signatures(keys, validationDate, PGPainless.getPolicy());
}
/**
@ -142,11 +112,11 @@ public class KeyRingInfo {
}
if (publicKey == getPublicKey()) {
return revocationSelfSignature == null;
return signatures.primaryKeyRevocation == null;
}
PGPSignature binding = mostRecentSubkeyBindings.get(keyId);
PGPSignature revocation = mostRecentSubkeyRevocations.get(keyId);
PGPSignature binding = signatures.subkeyBindings.get(keyId);
PGPSignature revocation = signatures.subkeyRevocations.get(keyId);
return binding != null && revocation == null;
}
@ -225,7 +195,7 @@ public class KeyRingInfo {
String primaryUserId = null;
Date modificationDate = null;
for (String userId : getValidUserIds()) {
PGPSignature signature = mostRecentUserIdSignatures.get(userId);
PGPSignature signature = signatures.userIdCertifications.get(userId);
PrimaryUserID subpacket = SignatureSubpacketsUtil.getPrimaryUserId(signature);
if (subpacket != null && subpacket.isPrimaryUserID()) {
// if there are multiple primary userIDs, return most recently signed
@ -235,6 +205,11 @@ public class KeyRingInfo {
}
}
}
// Workaround for keys with only one user-id but no primary user-id packet.
if (primaryUserId == null) {
return getValidUserIds().get(0);
}
return primaryUserId;
}
@ -261,8 +236,8 @@ public class KeyRingInfo {
}
public boolean isUserIdValid(String userId) {
PGPSignature certification = mostRecentUserIdSignatures.get(userId);
PGPSignature revocation = mostRecentUserIdRevocations.get(userId);
PGPSignature certification = signatures.userIdCertifications.get(userId);
PGPSignature revocation = signatures.userIdRevocations.get(userId);
return certification != null && revocation == null;
}
@ -285,34 +260,35 @@ public class KeyRingInfo {
}
public PGPSignature getCurrentDirectKeySelfSignature() {
return mostRecentSelfSignature;
return signatures.primaryKeySelfSignature;
}
public PGPSignature getRevocationSelfSignature() {
return revocationSelfSignature;
return signatures.primaryKeyRevocation;
}
public PGPSignature getCurrentUserIdCertification(String userId) {
return mostRecentUserIdSignatures.get(userId);
return signatures.userIdCertifications.get(userId);
}
public PGPSignature getUserIdRevocation(String userId) {
return mostRecentUserIdRevocations.get(userId);
return signatures.userIdRevocations.get(userId);
}
public PGPSignature getCurrentSubkeyBindingSignature(long keyId) {
return mostRecentSubkeyBindings.get(keyId);
return signatures.subkeyBindings.get(keyId);
}
public PGPSignature getSubkeyRevocationSignature(long keyId) {
return mostRecentSubkeyRevocations.get(keyId);
return signatures.subkeyRevocations.get(keyId);
}
public List<KeyFlag> getKeyFlagsOf(long keyId) {
if (getPublicKey().getKeyID() == keyId) {
if (mostRecentSelfSignature != null) {
KeyFlags flags = SignatureSubpacketsUtil.getKeyFlags(mostRecentSelfSignature);
PGPSignature directKeySignature = getCurrentDirectKeySelfSignature();
if (directKeySignature != null) {
KeyFlags flags = SignatureSubpacketsUtil.getKeyFlags(directKeySignature);
if (flags != null) {
return KeyFlag.fromBitmask(flags.getFlags());
}
@ -320,7 +296,15 @@ public class KeyRingInfo {
String primaryUserId = getPrimaryUserId();
if (primaryUserId != null) {
KeyFlags flags = SignatureSubpacketsUtil.getKeyFlags(mostRecentUserIdSignatures.get(primaryUserId));
KeyFlags flags = SignatureSubpacketsUtil.getKeyFlags(getCurrentUserIdCertification(primaryUserId));
if (flags != null) {
return KeyFlag.fromBitmask(flags.getFlags());
}
}
} else {
PGPSignature bindingSignature = getCurrentSubkeyBindingSignature(keyId);
if (bindingSignature != null) {
KeyFlags flags = SignatureSubpacketsUtil.getKeyFlags(bindingSignature);
if (flags != null) {
return KeyFlag.fromBitmask(flags.getFlags());
}
@ -334,7 +318,7 @@ public class KeyRingInfo {
return Collections.emptyList();
}
PGPSignature userIdCertification = mostRecentUserIdSignatures.get(userId);
PGPSignature userIdCertification = getCurrentUserIdCertification(userId);
if (userIdCertification == null) {
return Collections.emptyList();
}
@ -377,12 +361,14 @@ public class KeyRingInfo {
private PGPSignature getMostRecentSignature() {
Set<PGPSignature> allSignatures = new HashSet<>();
PGPSignature mostRecentSelfSignature = getCurrentDirectKeySelfSignature();
PGPSignature revocationSelfSignature = getRevocationSelfSignature();
if (mostRecentSelfSignature != null) allSignatures.add(mostRecentSelfSignature);
if (revocationSelfSignature != null) allSignatures.add(revocationSelfSignature);
allSignatures.addAll(mostRecentUserIdSignatures.values());
allSignatures.addAll(mostRecentUserIdRevocations.values());
allSignatures.addAll(mostRecentSubkeyBindings.values());
allSignatures.addAll(mostRecentSubkeyRevocations.values());
allSignatures.addAll(signatures.userIdCertifications.values());
allSignatures.addAll(signatures.userIdRevocations.values());
allSignatures.addAll(signatures.subkeyBindings.values());
allSignatures.addAll(signatures.subkeyRevocations.values());
PGPSignature mostRecent = null;
for (PGPSignature signature : allSignatures) {
@ -399,7 +385,7 @@ public class KeyRingInfo {
* @return revocation date or null
*/
public Date getRevocationDate() {
return revocationSelfSignature == null ? null : revocationSelfSignature.getCreationTime();
return getRevocationSelfSignature() == null ? null : getRevocationSelfSignature().getCreationTime();
}
/**
@ -409,8 +395,8 @@ public class KeyRingInfo {
*/
public Date getPrimaryKeyExpirationDate() {
Date lastExpiration = null;
if (mostRecentSelfSignature != null) {
lastExpiration = SignatureUtils.getKeyExpirationDate(getCreationDate(), mostRecentSelfSignature);
if (getCurrentDirectKeySelfSignature() != null) {
lastExpiration = SignatureUtils.getKeyExpirationDate(getCreationDate(), getCurrentDirectKeySelfSignature());
}
for (String userId : getValidUserIds()) {
@ -432,7 +418,7 @@ public class KeyRingInfo {
if (subkey == null) {
throw new IllegalArgumentException("No subkey with fingerprint " + fingerprint + " found.");
}
return SignatureUtils.getKeyExpirationDate(subkey.getCreationTime(), mostRecentSubkeyBindings.get(fingerprint.getKeyId()));
return SignatureUtils.getKeyExpirationDate(subkey.getCreationTime(), getCurrentSubkeyBindingSignature(fingerprint.getKeyId()));
}
/**
@ -489,4 +475,130 @@ public class KeyRingInfo {
}
return false;
}
public PGPPublicKey getEncryptionSubkey(EncryptionStream.Purpose purpose) {
Iterator<PGPPublicKey> subkeys = keys.getPublicKeys();
while (subkeys.hasNext()) {
PGPPublicKey subKey = subkeys.next();
if (!isKeyValidlyBound(subKey.getKeyID())) {
continue;
}
if (!subKey.isEncryptionKey()) {
continue;
}
List<KeyFlag> keyFlags = getKeyFlagsOf(subKey.getKeyID());
switch (purpose) {
case COMMUNICATIONS:
if (keyFlags.contains(KeyFlag.ENCRYPT_COMMS)) {
return subKey;
}
break;
case STORAGE:
if (keyFlags.contains(KeyFlag.ENCRYPT_STORAGE)) {
return subKey;
}
break;
case STORAGE_AND_COMMUNICATIONS:
if (keyFlags.contains(KeyFlag.ENCRYPT_COMMS) || keyFlags.contains(KeyFlag.ENCRYPT_STORAGE)) {
return subKey;
}
break;
}
}
return null;
}
public PGPPublicKey getEncryptionSubkey(String userId, EncryptionStream.Purpose purpose) {
if (userId != null) {
if (!isUserIdValid(userId)) {
throw new KeyValidationException(userId, getCurrentUserIdCertification(userId), getUserIdRevocation(userId));
}
}
return getEncryptionSubkey(purpose);
}
public PGPPublicKey getSigningSubkey() {
Iterator<PGPPublicKey> subkeys = keys.getPublicKeys();
while (subkeys.hasNext()) {
PGPPublicKey subKey = subkeys.next();
if (!isKeyValidlyBound(subKey.getKeyID())) {
continue;
}
if (!subKey.isEncryptionKey()) {
continue;
}
List<KeyFlag> keyFlags = getKeyFlagsOf(subKey.getKeyID());
if (keyFlags.contains(KeyFlag.SIGN_DATA)) {
return subKey;
}
}
return null;
}
public List<HashAlgorithm> getPreferredHashAlgorithms(String userId, long keyID) {
PGPSignature signature = getCurrentUserIdCertification(userId == null ? getPrimaryUserId() : userId);
if (signature == null) {
signature = getCurrentDirectKeySelfSignature();
}
if (signature == null) {
signature = getCurrentSubkeyBindingSignature(keyID);
}
if (signature == null) {
throw new IllegalStateException("No valid signature.");
}
return SignatureSubpacketsUtil.parsePreferredHashAlgorithms(signature);
}
public static class Signatures {
private final PGPSignature primaryKeyRevocation;
private final PGPSignature primaryKeySelfSignature;
private final Map<String, PGPSignature> userIdRevocations;
private final Map<String, PGPSignature> userIdCertifications;
private final Map<Long, PGPSignature> subkeyRevocations;
private final Map<Long, PGPSignature> subkeyBindings;
public Signatures(PGPKeyRing keyRing, Date evaluationDate, Policy policy) {
primaryKeyRevocation = SignaturePicker.pickCurrentRevocationSelfSignature(keyRing, evaluationDate);
primaryKeySelfSignature = SignaturePicker.pickCurrentDirectKeySelfSignature(keyRing, evaluationDate);
userIdRevocations = new HashMap<>();
userIdCertifications = new HashMap<>();
subkeyRevocations = new HashMap<>();
subkeyBindings = new HashMap<>();
for (Iterator<String> it = keyRing.getPublicKey().getUserIDs(); it.hasNext(); ) {
String userId = it.next();
PGPSignature revocation = SignaturePicker.pickCurrentUserIdRevocationSignature(keyRing, userId, evaluationDate);
if (revocation != null) {
userIdRevocations.put(userId, revocation);
}
PGPSignature certification = SignaturePicker.pickCurrentUserIdCertificationSignature(keyRing, userId, evaluationDate);
if (certification != null) {
userIdCertifications.put(userId, certification);
}
}
Iterator<PGPPublicKey> keys = keyRing.getPublicKeys();
keys.next(); // Skip primary key
while (keys.hasNext()) {
PGPPublicKey subkey = keys.next();
PGPSignature subkeyRevocation = SignaturePicker.pickCurrentSubkeyBindingRevocationSignature(keyRing, subkey, evaluationDate);
if (subkeyRevocation != null) {
subkeyRevocations.put(subkey.getKeyID(), subkeyRevocation);
}
PGPSignature subkeyBinding = SignaturePicker.pickCurrentSubkeyBindingSignature(keyRing, subkey, evaluationDate);
if (subkeyBinding != null) {
subkeyBindings.put(subkey.getKeyID(), subkeyBinding);
}
}
}
}
}

View File

@ -19,6 +19,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.pgpainless.algorithm.CompressionAlgorithm;
import org.pgpainless.algorithm.HashAlgorithm;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.util.NotationRegistry;
@ -38,6 +39,8 @@ public final class Policy {
SymmetricKeyAlgorithmPolicy.defaultSymmetricKeyEncryptionAlgorithmPolicy();
private SymmetricKeyAlgorithmPolicy symmetricKeyDecryptionAlgorithmPolicy =
SymmetricKeyAlgorithmPolicy.defaultSymmetricKeyDecryptionAlgorithmPolicy();
private CompressionAlgorithmPolicy compressionAlgorithmPolicy =
CompressionAlgorithmPolicy.defaultCompressionAlgorithmPolicy();
private final NotationRegistry notationRegistry = new NotationRegistry();
private Policy() {
@ -142,6 +145,17 @@ public final class Policy {
this.symmetricKeyDecryptionAlgorithmPolicy = policy;
}
public CompressionAlgorithmPolicy getCompressionAlgorithmPolicy() {
return compressionAlgorithmPolicy;
}
public void setCompressionAlgorithmPolicy(CompressionAlgorithmPolicy policy) {
if (policy == null) {
throw new NullPointerException("Compression policy cannot be null.");
}
this.compressionAlgorithmPolicy = policy;
}
public static final class SymmetricKeyAlgorithmPolicy {
private final SymmetricKeyAlgorithm defaultSymmetricKeyAlgorithm;
@ -297,6 +311,39 @@ public final class Policy {
}
}
public static final class CompressionAlgorithmPolicy {
private final CompressionAlgorithm defaultCompressionAlgorithm;
private final List<CompressionAlgorithm> acceptableCompressionAlgorithms;
public CompressionAlgorithmPolicy(CompressionAlgorithm defaultCompressionAlgorithm,
List<CompressionAlgorithm> acceptableCompressionAlgorithms) {
this.defaultCompressionAlgorithm = defaultCompressionAlgorithm;
this.acceptableCompressionAlgorithms = Collections.unmodifiableList(acceptableCompressionAlgorithms);
}
public CompressionAlgorithm defaultCompressionAlgorithm() {
return defaultCompressionAlgorithm;
}
public boolean isAcceptable(int compressionAlgorithmTag) {
return isAcceptable(CompressionAlgorithm.fromId(compressionAlgorithmTag));
}
public boolean isAcceptable(CompressionAlgorithm compressionAlgorithm) {
return acceptableCompressionAlgorithms.contains(compressionAlgorithm);
}
public static CompressionAlgorithmPolicy defaultCompressionAlgorithmPolicy() {
return new CompressionAlgorithmPolicy(CompressionAlgorithm.UNCOMPRESSED, Arrays.asList(
CompressionAlgorithm.UNCOMPRESSED,
CompressionAlgorithm.ZIP,
CompressionAlgorithm.BZIP2,
CompressionAlgorithm.ZLIB
));
}
}
/**
* Return the {@link NotationRegistry} of PGPainless.
* The notation registry is used to decide, whether or not a Notation is known or not.

View File

@ -59,7 +59,7 @@ public abstract class SelectSignatureFromKey {
* Criterion that checks if the signature is valid at the validation date.
* A signature is not valid if it was created after the validation date, or if it is expired at the validation date.
*
* creationTime less than or equal validationDate less than expirationDate.
* creationTime &le; validationDate &lt; expirationDate.
*
* @param validationDate validation date
* @return criterion implementation

View File

@ -25,8 +25,11 @@ import org.bouncycastle.bcpg.sig.SignatureCreationTime;
import org.bouncycastle.openpgp.PGPKeyRing;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSignature;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.SignatureType;
import org.pgpainless.exception.SignatureValidationException;
import org.pgpainless.key.util.RevocationAttributes;
import org.pgpainless.policy.Policy;
import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil;
import org.pgpainless.util.CollectionUtils;
@ -53,36 +56,19 @@ public class SignaturePicker {
* @return most recent, valid key revocation signature
*/
public static PGPSignature pickCurrentRevocationSelfSignature(PGPKeyRing keyRing, Date validationDate) {
Policy policy = PGPainless.getPolicy();
PGPPublicKey primaryKey = keyRing.getPublicKey();
List<PGPSignature> signatures = getSortedSignaturesOfType(primaryKey, SignatureType.KEY_REVOCATION);
PGPSignature mostCurrentValidSig = null;
for (PGPSignature signature : signatures) {
if (!SelectSignatureFromKey.isWellFormed().accept(signature, primaryKey, keyRing)) {
// Signature is not well-formed. Reject
try {
SignatureValidator.verifyKeyRevocationSignature(signature, primaryKey, policy, validationDate);
} catch (SignatureValidationException e) {
// Signature is not valid
continue;
}
if (!SelectSignatureFromKey.isCreatedBy(keyRing.getPublicKey()).accept(signature, primaryKey, keyRing)) {
// Revocation signature was not created by primary key
continue;
}
RevocationReason reason = SignatureSubpacketsUtil.getRevocationReason(signature);
if (reason != null && !RevocationAttributes.Reason.isHardRevocation(reason.getRevocationReason())) {
// reason code states soft revocation
if (!SelectSignatureFromKey.isValidAt(validationDate).accept(signature, primaryKey, keyRing)) {
// Soft revocation is either expired or not yet valid
continue;
}
}
if (!SelectSignatureFromKey.isValidKeyRevocationSignature(primaryKey).accept(signature, primaryKey, keyRing)) {
// sig does not check out
continue;
}
mostCurrentValidSig = signature;
}

View File

@ -20,6 +20,8 @@ import java.util.Arrays;
import java.util.Date;
import java.util.List;
import javax.annotation.Nullable;
import org.bouncycastle.bcpg.sig.Exportable;
import org.bouncycastle.bcpg.sig.Features;
import org.bouncycastle.bcpg.sig.IntendedRecipientFingerprint;
@ -44,7 +46,11 @@ import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureList;
import org.bouncycastle.openpgp.PGPSignatureSubpacketVector;
import org.bouncycastle.util.encoders.Hex;
import org.pgpainless.algorithm.CompressionAlgorithm;
import org.pgpainless.algorithm.HashAlgorithm;
import org.pgpainless.algorithm.KeyFlag;
import org.pgpainless.algorithm.SignatureSubpacket;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.signature.SignatureUtils;
@ -200,6 +206,17 @@ public class SignatureSubpacketsUtil {
return hashed(signature, SignatureSubpacket.preferredSymmetricAlgorithms);
}
public static List<SymmetricKeyAlgorithm> parsePreferredSymmetricKeyAlgorithms(PGPSignature signature) {
List<SymmetricKeyAlgorithm> algorithms = new ArrayList<>();
PreferredAlgorithms preferences = getPreferredSymmetricAlgorithms(signature);
if (preferences != null) {
for (int code : preferences.getPreferences()) {
algorithms.add(SymmetricKeyAlgorithm.fromId(code));
}
}
return algorithms;
}
/**
* Return the hash algorithm preferences from the signatures hashed area.
*
@ -210,6 +227,17 @@ public class SignatureSubpacketsUtil {
return hashed(signature, SignatureSubpacket.preferredHashAlgorithms);
}
public static List<HashAlgorithm> parsePreferredHashAlgorithms(PGPSignature signature) {
List<HashAlgorithm> algorithms = new ArrayList<>();
PreferredAlgorithms preferences = getPreferredHashAlgorithms(signature);
if (preferences != null) {
for (int code : preferences.getPreferences()) {
algorithms.add(HashAlgorithm.fromId(code));
}
}
return algorithms;
}
/**
* Return the compression algorithm preferences from the signatures hashed area.
*
@ -220,6 +248,17 @@ public class SignatureSubpacketsUtil {
return hashed(signature, SignatureSubpacket.preferredCompressionAlgorithms);
}
public static List<CompressionAlgorithm> parsePreferredCompressionAlgorithms(PGPSignature signature) {
List<CompressionAlgorithm> algorithms = new ArrayList<>();
PreferredAlgorithms preferences = getPreferredCompressionAlgorithms(signature);
if (preferences != null) {
for (int code : preferences.getPreferences()) {
algorithms.add(CompressionAlgorithm.fromId(code));
}
}
return algorithms;
}
/**
* Return the primary user-id subpacket from the signatures hashed area.
*
@ -240,6 +279,24 @@ public class SignatureSubpacketsUtil {
return hashed(signature, SignatureSubpacket.keyFlags);
}
/**
* Return a list of key flags carried by the signature.
* If the signature is null, or has no {@link KeyFlags} subpacket, return null.
*
* @param signature signature
* @return list of key flags
*/
public static List<KeyFlag> parseKeyFlags(@Nullable PGPSignature signature) {
if (signature == null) {
return null;
}
KeyFlags keyFlags = getKeyFlags(signature);
if (keyFlags == null) {
return null;
}
return KeyFlag.fromBitmask(keyFlags.getFlags());
}
/**
* Return the features subpacket from the signatures hashed area.
*

View File

@ -17,18 +17,23 @@ package org.bouncycastle;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless;
import org.pgpainless.key.util.KeyRingUtils;
import org.pgpainless.util.CollectionUtils;
public class PGPPublicKeyRingTest {
@ -57,4 +62,21 @@ public class PGPPublicKeyRingTest {
}
}
}
@Test
public void removeUserIdTest() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException {
String userId = "alice@wonderland.lit";
PGPSecretKeyRing secretKeyRing = PGPainless.generateKeyRing().simpleEcKeyRing(userId);
PGPPublicKeyRing publicKeys = KeyRingUtils.publicKeyRingFrom(secretKeyRing);
List<String> userIds = CollectionUtils.iteratorToList(publicKeys.getPublicKey().getUserIDs());
assertTrue(userIds.contains(userId));
PGPPublicKey publicKey = publicKeys.getPublicKey();
PGPSignature cert = publicKey.getSignaturesForID(userId).next();
publicKey = PGPPublicKey.removeCertification(publicKey, cert);
userIds = CollectionUtils.iteratorToList(publicKey.getUserIDs());
assertFalse(userIds.contains(userId));
}
}

View File

@ -41,12 +41,13 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.DocumentSignatureType;
import org.pgpainless.algorithm.KeyFlag;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.decryption_verification.DecryptionStream;
import org.pgpainless.decryption_verification.OpenPgpMetadata;
import org.pgpainless.implementation.ImplementationFactory;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.key.TestKeys;
import org.pgpainless.key.generation.KeySpec;
import org.pgpainless.key.generation.type.KeyType;
@ -155,26 +156,20 @@ public class EncryptDecryptTest {
EncryptionStream encryptor = PGPainless.encryptAndOrSign()
.onOutputStream(envelope)
.toRecipients(recipientPub)
.usingSecureAlgorithms()
.signWith(keyDecryptor, senderSec)
.signBinaryDocument()
.toRecipient(recipientPub)
.and()
.signInlineWith(keyDecryptor, senderSec, null, DocumentSignatureType.BINARY_DOCUMENT)
.noArmor();
Streams.pipeAll(new ByteArrayInputStream(secretMessage), encryptor);
encryptor.close();
byte[] encryptedSecretMessage = envelope.toByteArray();
OpenPgpMetadata encryptionResult = encryptor.getResult();
EncryptionResult encryptionResult = encryptor.getResult();
assertFalse(encryptionResult.getSignatures().isEmpty());
for (OpenPgpV4Fingerprint fingerprint : encryptionResult.getVerifiedSignatures().keySet()) {
assertTrue(BCUtil.keyRingContainsKeyWithId(senderPub, fingerprint.getKeyId()));
}
assertFalse(encryptionResult.getRecipientKeyIds().isEmpty());
for (long keyId : encryptionResult.getRecipientKeyIds()) {
assertTrue(BCUtil.keyRingContainsKeyWithId(recipientPub, keyId));
assertFalse(encryptionResult.getRecipients().isEmpty());
for (SubkeyIdentifier encryptionKey : encryptionResult.getRecipients()) {
assertTrue(BCUtil.keyRingContainsKeyWithId(recipientPub, encryptionKey.getKeyId()));
}
assertEquals(SymmetricKeyAlgorithm.AES_256, encryptionResult.getSymmetricKeyAlgorithm());
@ -214,15 +209,14 @@ public class EncryptDecryptTest {
ByteArrayOutputStream dummyOut = new ByteArrayOutputStream();
EncryptionStream signer = PGPainless.encryptAndOrSign().onOutputStream(dummyOut)
.doNotEncrypt()
.createDetachedSignature()
.signWith(keyRingProtector, signingKeys)
.signBinaryDocument()
.signDetachedWith(keyRingProtector, signingKeys)
.noArmor();
Streams.pipeAll(inputStream, signer);
signer.close();
OpenPgpMetadata metadata = signer.getResult();
Set<PGPSignature> signatureSet = metadata.getSignatures();
EncryptionResult metadata = signer.getResult();
Set<PGPSignature> signatureSet = metadata.getDetachedSignatures().get(metadata.getDetachedSignatures().keySet().iterator().next());
ByteArrayOutputStream sigOut = new ByteArrayOutputStream();
ArmoredOutputStream armorOut = ArmoredOutputStreamFactory.get(sigOut);
signatureSet.iterator().next().encode(armorOut);
@ -244,8 +238,8 @@ public class EncryptDecryptTest {
Streams.pipeAll(verifier, dummyOut);
verifier.close();
metadata = verifier.getResult();
assertFalse(metadata.getVerifiedSignatures().isEmpty());
OpenPgpMetadata decryptionResult = verifier.getResult();
assertFalse(decryptionResult.getVerifiedSignatures().isEmpty());
}
@ParameterizedTest
@ -259,8 +253,7 @@ public class EncryptDecryptTest {
ByteArrayOutputStream signOut = new ByteArrayOutputStream();
EncryptionStream signer = PGPainless.encryptAndOrSign().onOutputStream(signOut)
.doNotEncrypt()
.signWith(keyRingProtector, signingKeys)
.signBinaryDocument()
.signInlineWith(keyRingProtector, signingKeys)
.asciiArmor();
Streams.pipeAll(inputStream, signer);
signer.close();
@ -344,6 +337,6 @@ public class EncryptDecryptTest {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
assertThrows(IllegalArgumentException.class, () ->
PGPainless.encryptAndOrSign().onOutputStream(outputStream)
.toRecipients(publicKeys));
.toRecipient(publicKeys));
}
}

View File

@ -37,8 +37,8 @@ public class EncryptionStreamClosedTest {
OutputStream out = new ByteArrayOutputStream();
EncryptionStream stream = PGPainless.encryptAndOrSign()
.onOutputStream(out)
.forPassphrases(Passphrase.fromPassword("dummy"))
.usingSecureAlgorithms()
.forPassphrase(Passphrase.fromPassword("dummy"))
.and()
.doNotSign()
.asciiArmor();

View File

@ -69,8 +69,8 @@ public class FileInfoTest {
ByteArrayOutputStream dataOut = new ByteArrayOutputStream();
EncryptionStream encryptionStream = PGPainless.encryptAndOrSign()
.onOutputStream(dataOut, fileInfo)
.toRecipients(publicKeys)
.usingSecureAlgorithms()
.toRecipient(publicKeys)
.and()
.doNotSign()
.noArmor();

View File

@ -31,6 +31,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.util.io.Streams;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.DocumentSignatureType;
import org.pgpainless.key.TestKeys;
import org.pgpainless.key.generation.type.rsa.RsaLength;
import org.pgpainless.key.protection.SecretKeyRingProtector;
@ -111,11 +112,9 @@ public class LengthTest {
OutputStream encryptor = PGPainless.encryptAndOrSign()
.onOutputStream(envelope)
.toRecipients(recipientPub)
// .doNotEncrypt()
.usingSecureAlgorithms()
.signWith(keyDecryptor, senderSec)
.signBinaryDocument()
.toRecipient(recipientPub)
.and()
.signInlineWith(keyDecryptor, senderSec, "simplejid@server.tld", DocumentSignatureType.BINARY_DOCUMENT)
.noArmor();
Streams.pipeAll(new ByteArrayInputStream(secretMessage), encryptor);

View File

@ -37,7 +37,7 @@ import org.bouncycastle.util.io.Streams;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.CompressionAlgorithm;
import org.pgpainless.algorithm.DocumentSignatureType;
import org.pgpainless.algorithm.HashAlgorithm;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.decryption_verification.DecryptionStream;
@ -70,10 +70,11 @@ public class SigningTest {
EncryptionStream encryptionStream = PGPainless.encryptAndOrSign(EncryptionStream.Purpose.STORAGE)
.onOutputStream(out)
.toRecipients(keys)
.andToSelf(KeyRingUtils.publicKeyRingFrom(cryptieKeys))
.usingAlgorithms(SymmetricKeyAlgorithm.AES_192, HashAlgorithm.SHA384, CompressionAlgorithm.ZIP)
.signWith(SecretKeyRingProtector.unlockSingleKeyWith(TestKeys.CRYPTIE_PASSPHRASE, cryptieSigningKey), cryptieKeys)
.signCanonicalText()
.and()
.toRecipient(KeyRingUtils.publicKeyRingFrom(cryptieKeys))
.and()
.signInlineWith(SecretKeyRingProtector.unlockSingleKeyWith(TestKeys.CRYPTIE_PASSPHRASE, cryptieSigningKey),
cryptieKeys, TestKeys.CRYPTIE_UID, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)
.asciiArmor();
byte[] messageBytes = "This message is signed and encrypted to Romeo and Juliet.".getBytes(StandardCharsets.UTF_8);

View File

@ -193,8 +193,7 @@ public class ChangeSecretKeyRingPassphraseTest {
ByteArrayOutputStream dummy = new ByteArrayOutputStream();
EncryptionStream stream = PGPainless.encryptAndOrSign().onOutputStream(dummy)
.doNotEncrypt()
.signWith(PasswordBasedSecretKeyRingProtector.forKey(keyRing, passphrase), keyRing)
.signBinaryDocument()
.signInlineWith(PasswordBasedSecretKeyRingProtector.forKey(keyRing, passphrase), keyRing)
.noArmor();
Streams.pipeAll(new ByteArrayInputStream(dummyMessage.getBytes()), stream);

View File

@ -36,7 +36,7 @@ public class MultiPassphraseSymmetricEncryptionTest {
@ParameterizedTest
@MethodSource("org.pgpainless.util.TestUtil#provideImplementationFactories")
@Disabled
public void test(ImplementationFactory implementationFactory) throws IOException, PGPException {
public void encryptDecryptWithMultiplePassphrases(ImplementationFactory implementationFactory) throws IOException, PGPException {
ImplementationFactory.setFactoryImplementation(implementationFactory);
String message = "Here we test if during decryption of a message that was encrypted with two passphrases, " +
"the decryptor finds the session key encrypted for the right passphrase.";
@ -44,8 +44,10 @@ public class MultiPassphraseSymmetricEncryptionTest {
ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream();
EncryptionStream encryptor = PGPainless.encryptAndOrSign()
.onOutputStream(ciphertextOut)
.forPassphrases(Passphrase.fromPassword("p1"), Passphrase.fromPassword("p2"))
.usingSecureAlgorithms()
.forPassphrase(Passphrase.fromPassword("p1"))
.and()
.forPassphrase(Passphrase.fromPassword("p2"))
.and()
.doNotSign()
.noArmor();

View File

@ -30,6 +30,7 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.pgpainless.PGPainless;
import org.pgpainless.decryption_verification.DecryptionStream;
import org.pgpainless.encryption_signing.EncryptionBuilderInterface;
import org.pgpainless.encryption_signing.EncryptionStream;
import org.pgpainless.implementation.ImplementationFactory;
import org.pgpainless.key.TestKeys;
@ -46,7 +47,7 @@ public class SymmetricEncryptionTest {
@ParameterizedTest
@MethodSource("org.pgpainless.util.TestUtil#provideImplementationFactories")
public void test(ImplementationFactory implementationFactory) throws IOException, PGPException {
public void encryptWithKeyAndPassphrase_DecryptWithKey(ImplementationFactory implementationFactory) throws IOException, PGPException {
ImplementationFactory.setFactoryImplementation(implementationFactory);
byte[] plaintext = "This is a secret message".getBytes(StandardCharsets.UTF_8);
ByteArrayInputStream plaintextIn = new ByteArrayInputStream(plaintext);
@ -54,12 +55,13 @@ public class SymmetricEncryptionTest {
Passphrase encryptionPassphrase = Passphrase.fromPassword("greenBeans");
ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream();
EncryptionStream encryptor = PGPainless.encryptAndOrSign().onOutputStream(ciphertextOut)
.forPassphrases(encryptionPassphrase)
EncryptionBuilderInterface.Armor armor = PGPainless.encryptAndOrSign().onOutputStream(ciphertextOut)
.forPassphrase(encryptionPassphrase)
.and()
.toRecipients(encryptionKey)
.usingSecureAlgorithms()
.doNotSign()
.toRecipient(encryptionKey)
.and()
.doNotSign();
EncryptionStream encryptor = armor
.noArmor();
Streams.pipeAll(plaintextIn, encryptor);

View File

@ -56,6 +56,6 @@ public class TestEncryptCommsStorageFlagsDifferentiated {
.onOutputStream(out);
// since the key does not carry the flag ENCRYPT_COMMS, it cannot be used by the stream.
assertThrows(IllegalArgumentException.class, () -> builder.toRecipients(publicKeys));
assertThrows(IllegalArgumentException.class, () -> builder.toRecipient(publicKeys));
}
}

View File

@ -28,7 +28,7 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.util.io.Streams;
import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless;
import org.pgpainless.decryption_verification.OpenPgpMetadata;
import org.pgpainless.encryption_signing.EncryptionResult;
import org.pgpainless.encryption_signing.EncryptionStream;
import org.pgpainless.key.WeirdKeys;
import org.pgpainless.key.util.KeyRingUtils;
@ -57,16 +57,16 @@ public class TestTwoSubkeysEncryption {
ByteArrayOutputStream out = new ByteArrayOutputStream();
EncryptionStream encryptionStream = PGPainless.encryptAndOrSign(EncryptionStream.Purpose.STORAGE)
.onOutputStream(out)
.toRecipients(publicKeys)
.usingSecureAlgorithms()
.toRecipient(publicKeys)
.and()
.doNotSign()
.noArmor();
Streams.pipeAll(getPlainIn(), encryptionStream);
encryptionStream.close();
OpenPgpMetadata metadata = encryptionStream.getResult();
EncryptionResult metadata = encryptionStream.getResult();
assertEquals(2, metadata.getRecipientKeyIds().size());
assertEquals(2, metadata.getRecipients().size());
}
}

View File

@ -22,17 +22,20 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSecretKey;
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.KeyFlag;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.encryption_signing.EncryptionBuilderInterface;
@ -40,6 +43,7 @@ import org.pgpainless.encryption_signing.EncryptionStream;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.protection.KeyRingProtectionSettings;
import org.pgpainless.key.protection.CachingSecretKeyRingProtector;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.util.Passphrase;
import picocli.CommandLine;
@ -86,17 +90,26 @@ public class Encrypt implements Runnable {
System.exit(19);
}
PGPPublicKeyRing[] publicKeys = new PGPPublicKeyRing[certs.length];
PGPPublicKeyRing[] pubKeysArray = new PGPPublicKeyRing[certs.length];
for (int i = 0 ; i < certs.length; i++) {
try (InputStream fileIn = new FileInputStream(certs[i])) {
PGPPublicKeyRing publicKey = PGPainless.readKeyRing().publicKeyRing(fileIn);
publicKeys[i] = publicKey;
pubKeysArray[i] = publicKey;
} catch (IOException e) {
err_ln("Cannot read certificate " + certs[i].getName());
err_ln(e.getMessage());
System.exit(1);
}
}
PGPPublicKeyRingCollection publicKeys;
try {
publicKeys = new PGPPublicKeyRingCollection(Arrays.asList(pubKeysArray));
} catch (IOException | PGPException e) {
err_ln("Cannot construct public key collection.");
err_ln(e.getMessage());
System.exit(1);
return;
}
PGPSecretKeyRing[] secretKeys = new PGPSecretKeyRing[signWith.length];
for (int i = 0; i < signWith.length; i++) {
try (FileInputStream fileIn = new FileInputStream(signWith[i])) {
@ -140,25 +153,32 @@ public class Encrypt implements Runnable {
}
}
EncryptionBuilderInterface.DetachedSign builder = PGPainless.encryptAndOrSign()
EncryptionBuilderInterface.ToRecipientsOrSign builder = PGPainless.encryptAndOrSign()
.onOutputStream(System.out)
.toRecipients(publicKeys)
.and()
.forPassphrases(passphraseArray)
.usingSecureAlgorithms();
EncryptionBuilderInterface.Armor builder_armor;
if (signWith.length != 0) {
EncryptionBuilderInterface.DocumentType documentType = builder.signWith(new CachingSecretKeyRingProtector(passphraseMap,
KeyRingProtectionSettings.secureDefaultSettings(), null), secretKeys);
if (type == Type.text || type == Type.mime) {
builder_armor = documentType.signCanonicalText();
} else {
builder_armor = documentType.signBinaryDocument();
}
} else {
builder_armor = builder.doNotSign();
.and();
for (Passphrase passphrase : passphraseArray) {
builder = builder.forPassphrase(passphrase).and();
}
EncryptionBuilderInterface.Armor builder_armor = null;
EncryptionBuilderInterface.SignWith builder1 = builder;
try {
if (signWith.length != 0) {
for (int i = 0; i < signWith.length; i++) {
PGPSecretKeyRing secretKeyRing = secretKeys[i];
EncryptionBuilderInterface.AdditionalSignWith additionalSignWith = builder1.signInlineWith(
SecretKeyRingProtector.unlockAllKeysWith(
passphraseMap.get(secretKeyRing.getPublicKey().getKeyID()),
secretKeyRing),
secretKeyRing, null,
type == Type.text || type == Type.mime ?
DocumentSignatureType.CANONICAL_TEXT_DOCUMENT : DocumentSignatureType.BINARY_DOCUMENT);
builder_armor = additionalSignWith;
builder1 = additionalSignWith.and();
}
} else {
builder_armor = builder.doNotSign();
}
EncryptionStream encryptionStream = !armor ? builder_armor.noArmor() : builder_armor.asciiArmor();
Streams.pipeAll(System.in, encryptionStream);