1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2025-09-15 13:19:38 +02:00

Compare commits

...

80 commits

Author SHA1 Message Date
b216fd9d10
PGPainless 1.4.0-rc1 2022-11-03 12:34:52 +01:00
e2fc9ccb9c
Update changelog 2022-11-03 12:29:50 +01:00
cdef935974
Add comment for ArmorUtils method 2022-11-03 12:07:26 +01:00
c20e80f716 Implement efficient read(buf,off,len) for DelayedInputStream 2022-11-03 12:04:40 +01:00
dfb7d068bd Small clean-ups in OpenPgpMessageInputStream 2022-11-03 11:56:38 +01:00
3e120fbf7f Properly handle failed decryption caused by removed private keys 2022-11-03 11:56:38 +01:00
25fd3fa1d6 Move classes related to GNU dummy keys to gnupg package 2022-11-03 11:56:38 +01:00
b74e7bcb7c Add test for decryption with removed private key 2022-11-03 11:56:38 +01:00
3b9159e632 Rename GnuPGDummyExtension + GnuPGDummyKeyUtil 2022-11-03 11:56:38 +01:00
b5bd4875ae Use S2K usage SHA1 in GnuDummyKeyUtil 2022-11-03 11:56:38 +01:00
54ed7a5387 Add documentation to GnuDummyKeyUtil 2022-11-03 11:56:38 +01:00
489419459a Add and test GnuDummyKeyUtil 2022-11-03 11:56:38 +01:00
220f186336 Move CachingBcPublicKeyDataDecryptorFactoryTest to correct package 2022-11-03 11:56:38 +01:00
4b8367dcca Fix HardwareSecurity.getIdsOfHardwareBackedKeys() 2022-11-03 11:56:38 +01:00
09aaa9fa4e Fix CachingBcPublicKeyDataDecryptorFactory 2022-11-03 11:56:38 +01:00
1be4d8e5c4 Implement caching PublicKeyDataDecryptorFactory 2022-11-03 11:56:38 +01:00
d94fa6b74b Add comments 2022-11-03 11:56:38 +01:00
d5fcb3bc29 Add (commented-out) read(buf, off, len) implementation for DelayedTeeInputStream 2022-11-03 11:56:38 +01:00
6716e363c7 Allow injection of different syntax into PDA 2022-11-03 11:56:38 +01:00
291f59b9e4 Clean up old unused code 2022-11-03 11:56:38 +01:00
a0c8de57e9 More cleanup and better error reporting 2022-11-03 11:56:38 +01:00
3c94f3677f Rename *Alphabet to *Symbol and add javadoc 2022-11-03 11:56:38 +01:00
a9f67c508f Separate out syntax logic 2022-11-03 11:56:38 +01:00
c4e937c0f9 Improve syntax error reporting 2022-11-03 11:56:38 +01:00
0feafbf7ed Remove debugging fields 2022-11-03 11:56:38 +01:00
4aaecb7df8 Remove superfluous states 2022-11-03 11:56:38 +01:00
d8ff266406 Handle unknown packet versions gracefully 2022-11-03 11:56:38 +01:00
651ca93f90 Add missing REUSE license headers 2022-11-03 11:56:38 +01:00
08ceee9243 Wrap MalformedOpenPgpMessageException in BadData 2022-11-03 11:56:38 +01:00
160a5fe8ff Fix last two broken tests 2022-11-03 11:56:38 +01:00
4c6edc067b Fix more tests 2022-11-03 11:56:38 +01:00
2a4bc03555 Only check message integrity once 2022-11-03 11:56:38 +01:00
9ec4c47309 Delete old DecryptionStreamFactory 2022-11-03 11:56:38 +01:00
09d036f17b Fix CRCing test and fully depend on new stream for decryption 2022-11-03 11:56:38 +01:00
7aaeb8ccfd Further increase coverage of PDA class 2022-11-03 11:56:38 +01:00
bb1b154745 Add more direct PDA tests 2022-11-03 11:56:38 +01:00
9dbee67304 Rename automaton package to syntax_check 2022-11-03 11:56:38 +01:00
f67f1954e7 Add detailled logging to OpenPgpMessageInputStream 2022-11-03 11:56:38 +01:00
789a0086c9 Enable logging in tests 2022-11-03 11:56:38 +01:00
8fb8aa1a30 Throw UnacceptableAlgEx for unencrypted encData 2022-11-03 11:56:38 +01:00
0d62cea8a5 Implement custom decryptor factories in pda 2022-11-03 11:56:38 +01:00
a60917549d Identify custom decryptor factories by subkey id 2022-11-03 11:56:38 +01:00
02c68460a9 Make map final 2022-11-03 11:56:38 +01:00
24ed479b87 Change HardwareSecurity DecryptionCallback to emit key-id 2022-11-03 11:56:38 +01:00
0f5103577e Implement exploratory support for custom decryption factories
This may enable decryption of messages with hardware-backed keys
2022-11-03 11:56:38 +01:00
f15040dae0 WIP: Explore Hardware Decryption 2022-11-03 11:56:38 +01:00
0f768a7258 Work on postponed keys 2022-11-03 11:56:38 +01:00
5c61559647 Reuse *SignatureCheck class 2022-11-03 11:56:38 +01:00
0a7c76a2dd Enfore max recursion depth and fix CRC test 2022-11-03 11:56:38 +01:00
6324455cf5 Fix NPEs and expose decryption keys 2022-11-03 11:56:38 +01:00
513ab0e3ed Fix checkstyle issues 2022-11-03 11:56:38 +01:00
8a9ebdbb3e Reinstate integrity-protection and fix tests
Integrity Protection is now checked when reading from the stream,
not only when closing.
2022-11-03 11:56:38 +01:00
da6582e1d3 Properly expose signatures 2022-11-03 11:56:38 +01:00
78e607ab85 Cleaning up and collect signature verifications 2022-11-03 11:56:38 +01:00
f4ce669d44 It was the buffering. 2022-11-03 11:56:37 +01:00
ef310f201f Create TeeBCPGInputStream to move teeing logic out of OpenPgpMessageInputStream 2022-11-03 11:56:05 +01:00
c65e484bb4 2/3 the way to working sig verification 2022-11-03 11:56:05 +01:00
37dc362cc3 WIP: So close to working notarizations 2022-11-03 11:56:04 +01:00
b31d72bd65 Reformat KeyRingReader 2022-11-03 11:55:35 +01:00
9eea028cf7 Suppress DefaultCharset warning 2022-11-03 11:55:35 +01:00
17f90eb721 Convert links in javadoc to html 2022-11-03 11:55:35 +01:00
73cdf34b02 DO NOT MERGE: Disable broken test 2022-11-03 11:55:35 +01:00
6809a490c1 Remove unnecessary throws declarations 2022-11-03 11:55:34 +01:00
be8439532d Use BCs Arrays.constantTimeAreEqual(char[], char[]) 2022-11-03 11:54:24 +01:00
fe767389a0 Fix ModificationDetectionException by not calling PGPUtil.getDecoderStream() 2022-11-03 11:54:24 +01:00
9ba3fcd8b0 SIGNATURE VERIFICATION IN OPENPGP SUCKS BIG TIME 2022-11-03 11:53:43 +01:00
c40a7976e2 Wip 2022-11-03 11:53:43 +01:00
f614c325cb Wip: Work on OPS verification 2022-11-03 11:53:43 +01:00
7cb22f1530 Fix checkstyle issues 2022-11-03 11:53:43 +01:00
714e424eac Wip: Introduce MessageMetadata class 2022-11-03 11:53:43 +01:00
a3957d3372 WIP: Play around with TeeInputStreams 2022-11-03 11:53:43 +01:00
61949240b3 WIP: Add LayerMetadata class 2022-11-03 11:53:43 +01:00
ef33d9d584 Implement experimental signature verification (correctness only) 2022-11-03 11:53:43 +01:00
3ac17281ea Add read(b,off,len) 2022-11-03 11:53:43 +01:00
8625b2086c Clean close() method 2022-11-03 11:53:42 +01:00
80e98a02ac Work on getting signature verification to function again 2022-11-03 11:53:42 +01:00
8d6db322a1 Fix tests 2022-11-03 11:53:42 +01:00
6233ac61e6 WIP: Replace nesting with independent instancing 2022-11-03 11:53:42 +01:00
60d6289c4d WIP: Implement custom PGPDecryptionStream 2022-11-03 11:53:42 +01:00
7b76a9162d Add Pushdown Automaton for checking OpenPGP message syntax
The automaton implements what is described in
https://github.com/pgpainless/pgpainless/blob/main/misc/OpenPGPMessageFormat.md

However, some differences exist to adopt it to BouncyCastle

Part of #237
2022-11-03 11:53:42 +01:00
75 changed files with 4931 additions and 1151 deletions

View file

@ -5,6 +5,35 @@ SPDX-License-Identifier: CC0-1.0
# PGPainless Changelog
## 1.4.0-rc1
- Reimplement message consumption via new `OpenPgpMessageInputStream`
- Fix validation of prepended signatures (#314)
- Fix validation of nested signatures (#319)
- Reject malformed messages (#237)
- Utilize new `PDA` syntax verifier class
- Allow for custom message syntax via `Syntax` class
- Gracefully handle `UnsupportedPacketVersionException` for signatures
- Allow plugin decryption code (e.g. to add support for hardware-backed keys (see #318))
- Add `HardwareSecurity` utility class
- Add `GnuPGDummyKeyUtil` which can be used to mimic GnuPGs proprietary S2K extensions
for keys which were placed on hardware tokens
- Add `OpenPgpPacket` enum class to enumerate available packet tags
- Remove old decryption classes in favor of new implementation
- Removed `DecryptionStream` class and replaced with new abstract class
- Removed `DecryptionStreamFactory`
- Removed `FinalIOException`
- Removed `MissingLiteralDataException` (replaced by `MalformedOpenPgpMessageException`)
- Introduce `MessageMetadata` class as potential future replacement for `OpenPgpMetadata`.
- can be obtained via `((OpenPgpMessageInputStream) decryptionStream).getMetadata();`
- Add `CachingBcPublicKeyDataDecryptorFactory` which can be extended to prevent costly decryption
of session keys
- Fix: Only verify message integrity once
- Remove unnecessary `@throws` declarations on `KeyRingReader` methods
- Remove unnecessary `@throws` declarations on `KeyRingUtils` methods
- Add `KeyIdUtil.formatKeyId(long id)` to format hexadecimal key-ids.
- Add `KeyRingUtils.publicKeys(PGPKeyRing keys)`
- Remove `BCUtil` class
## 1.3.8
- Bump `bcprov` to `1.72`
- Bump `bcpg` to `1.72.1`

View file

@ -191,7 +191,7 @@ repositories {
}
dependencies {
implementation 'org.pgpainless:pgpainless-core:1.3.8'
implementation 'org.pgpainless:pgpainless-core:1.4.0-rc1'
}
```

View file

@ -0,0 +1,91 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.bouncycastle;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.util.encoders.Hex;
import org.pgpainless.decryption_verification.CustomPublicKeyDataDecryptorFactory;
import org.pgpainless.key.SubkeyIdentifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
/**
* Implementation of the {@link PublicKeyDataDecryptorFactory} which caches decrypted session keys.
* That way, if a message needs to be decrypted multiple times, expensive private key operations can be omitted.
*
* This implementation changes the behavior or {@link #recoverSessionData(int, byte[][])} to first return any
* cache hits.
* If no hit is found, the method call is delegated to the underlying {@link PublicKeyDataDecryptorFactory}.
* The result of that is then placed in the cache and returned.
*/
public class CachingBcPublicKeyDataDecryptorFactory
extends BcPublicKeyDataDecryptorFactory
implements CustomPublicKeyDataDecryptorFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(CachingBcPublicKeyDataDecryptorFactory.class);
private final Map<String, byte[]> cachedSessionKeys = new HashMap<>();
private final SubkeyIdentifier decryptionKey;
public CachingBcPublicKeyDataDecryptorFactory(PGPPrivateKey privateKey, SubkeyIdentifier decryptionKey) {
super(privateKey);
this.decryptionKey = decryptionKey;
}
@Override
public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException {
byte[] sessionKey = lookupSessionKeyData(secKeyData);
if (sessionKey == null) {
LOGGER.debug("Cache miss for encrypted session key " + Hex.toHexString(secKeyData[0]));
sessionKey = costlyRecoverSessionData(keyAlgorithm, secKeyData);
cacheSessionKeyData(secKeyData, sessionKey);
} else {
LOGGER.debug("Cache hit for encrypted session key " + Hex.toHexString(secKeyData[0]));
}
return sessionKey;
}
public byte[] costlyRecoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException {
return super.recoverSessionData(keyAlgorithm, secKeyData);
}
private byte[] lookupSessionKeyData(byte[][] secKeyData) {
byte[] sk = secKeyData[0];
String key = Base64.toBase64String(sk);
byte[] sessionKey = cachedSessionKeys.get(key);
return copy(sessionKey);
}
private void cacheSessionKeyData(byte[][] secKeyData, byte[] sessionKey) {
byte[] sk = secKeyData[0];
String key = Base64.toBase64String(sk);
cachedSessionKeys.put(key, copy(sessionKey));
}
private static byte[] copy(byte[] bytes) {
if (bytes == null) {
return null;
}
byte[] copy = new byte[bytes.length];
System.arraycopy(bytes, 0, copy, 0, copy.length);
return copy;
}
public void clear() {
cachedSessionKeys.clear();
}
@Override
public SubkeyIdentifier getSubkeyIdentifier() {
return decryptionKey;
}
}

View file

@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Classes which could be upstreamed to BC at some point.
*/
package org.bouncycastle;

View file

@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.gnupg;
import org.bouncycastle.bcpg.S2K;
public enum GnuPGDummyExtension {
/**
* Do not store the secret part at all.
*/
NO_PRIVATE_KEY(S2K.GNU_PROTECTION_MODE_NO_PRIVATE_KEY),
/**
* A stub to access smartcards.
*/
DIVERT_TO_CARD(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD),
;
private final int id;
GnuPGDummyExtension(int id) {
this.id = id;
}
public int getId() {
return id;
}
}

View file

@ -0,0 +1,209 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.gnupg;
import org.bouncycastle.bcpg.PublicKeyPacket;
import org.bouncycastle.bcpg.S2K;
import org.bouncycastle.bcpg.SecretKeyPacket;
import org.bouncycastle.bcpg.SecretSubkeyPacket;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.pgpainless.key.SubkeyIdentifier;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* This class can be used to remove private keys from secret software-keys by replacing them with
* stub secret keys in the style of GnuPGs proprietary extensions.
*
* @see <a href="https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/DETAILS;hb=HEAD#l1489">
* GnuPGs doc/DETAILS - GNU extensions to the S2K algorithm</a>
*/
public final class GnuPGDummyKeyUtil {
private GnuPGDummyKeyUtil() {
}
/**
* Return the key-ids of all keys which appear to be stored on a hardware token / smartcard by GnuPG.
* Note, that this functionality is based on GnuPGs proprietary S2K extensions, which are not strictly required
* for dealing with hardware-backed keys.
*
* @param secretKeys secret keys
* @return set of keys with S2K type GNU_DUMMY_S2K and protection mode DIVERT_TO_CARD
*/
public static Set<SubkeyIdentifier> getIdsOfKeysWithGnuPGS2KDivertedToCard(PGPSecretKeyRing secretKeys) {
Set<SubkeyIdentifier> hardwareBackedKeys = new HashSet<>();
for (PGPSecretKey secretKey : secretKeys) {
S2K s2K = secretKey.getS2K();
if (s2K == null) {
continue;
}
int type = s2K.getType();
int mode = s2K.getProtectionMode();
// TODO: Is GNU_DUMMY_S2K appropriate?
if (type == S2K.GNU_DUMMY_S2K && mode == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD) {
SubkeyIdentifier hardwareBackedKey = new SubkeyIdentifier(secretKeys, secretKey.getKeyID());
hardwareBackedKeys.add(hardwareBackedKey);
}
}
return hardwareBackedKeys;
}
/**
* Modify the given {@link PGPSecretKeyRing}.
*
* @param secretKeys secret keys
* @return builder
*/
public static Builder modify(PGPSecretKeyRing secretKeys) {
return new Builder(secretKeys);
}
public static final class Builder {
private final PGPSecretKeyRing keys;
private Builder(PGPSecretKeyRing keys) {
this.keys = keys;
}
/**
* Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with
* GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#NO_PRIVATE_KEY}.
*
* @param filter filter to select keys for removal
* @return modified key ring
*/
public PGPSecretKeyRing removePrivateKeys(KeyFilter filter) {
return replacePrivateKeys(GnuPGDummyExtension.NO_PRIVATE_KEY, null, filter);
}
/**
* Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with
* GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#DIVERT_TO_CARD}.
* This method will set the serial number of the card to 0x00000000000000000000000000000000.
*
* NOTE: This method does not actually move any keys to a card.
*
* @param filter filter to select keys for removal
* @return modified key ring
*/
public PGPSecretKeyRing divertPrivateKeysToCard(KeyFilter filter) {
return divertPrivateKeysToCard(filter, new byte[16]);
}
/**
* Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with
* GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#DIVERT_TO_CARD}.
* This method will include the card serial number into the encoded dummy key.
*
* NOTE: This method does not actually move any keys to a card.
*
* @param filter filter to select keys for removal
* @param cardSerialNumber serial number of the card (at most 16 bytes long)
* @return modified key ring
*/
public PGPSecretKeyRing divertPrivateKeysToCard(KeyFilter filter, byte[] cardSerialNumber) {
if (cardSerialNumber != null && cardSerialNumber.length > 16) {
throw new IllegalArgumentException("Card serial number length cannot exceed 16 bytes.");
}
return replacePrivateKeys(GnuPGDummyExtension.DIVERT_TO_CARD, cardSerialNumber, filter);
}
private PGPSecretKeyRing replacePrivateKeys(GnuPGDummyExtension extension, byte[] serial, KeyFilter filter) {
byte[] encodedSerial = serial != null ? encodeSerial(serial) : null;
S2K s2k = extensionToS2K(extension);
List<PGPSecretKey> secretKeyList = new ArrayList<>();
for (PGPSecretKey secretKey : keys) {
if (!filter.filter(secretKey.getKeyID())) {
// No conversion, do not modify subkey
secretKeyList.add(secretKey);
continue;
}
PublicKeyPacket publicKeyPacket = secretKey.getPublicKey().getPublicKeyPacket();
if (secretKey.isMasterKey()) {
SecretKeyPacket keyPacket = new SecretKeyPacket(publicKeyPacket,
0, SecretKeyPacket.USAGE_SHA1, s2k, null, encodedSerial);
PGPSecretKey onCard = new PGPSecretKey(keyPacket, secretKey.getPublicKey());
secretKeyList.add(onCard);
} else {
SecretSubkeyPacket keyPacket = new SecretSubkeyPacket(publicKeyPacket,
0, SecretKeyPacket.USAGE_SHA1, s2k, null, encodedSerial);
PGPSecretKey onCard = new PGPSecretKey(keyPacket, secretKey.getPublicKey());
secretKeyList.add(onCard);
}
}
return new PGPSecretKeyRing(secretKeyList);
}
private byte[] encodeSerial(@Nonnull byte[] serial) {
byte[] encoded = new byte[serial.length + 1];
encoded[0] = (byte) (serial.length & 0xff);
System.arraycopy(serial, 0, encoded, 1, serial.length);
return encoded;
}
private S2K extensionToS2K(@Nonnull GnuPGDummyExtension extension) {
return S2K.gnuDummyS2K(extension == GnuPGDummyExtension.DIVERT_TO_CARD ?
S2K.GNUDummyParams.divertToCard() : S2K.GNUDummyParams.noPrivateKey());
}
}
/**
* Filter for selecting keys.
*/
@FunctionalInterface
public interface KeyFilter {
/**
* Return true, if the given key should be selected, false otherwise.
*
* @param keyId id of the key
* @return select
*/
boolean filter(long keyId);
/**
* Select any key.
*
* @return filter
*/
static KeyFilter any() {
return keyId -> true;
}
/**
* Select only the given keyId.
*
* @param onlyKeyId only acceptable key id
* @return filter
*/
static KeyFilter only(long onlyKeyId) {
return keyId -> keyId == onlyKeyId;
}
/**
* Select all keyIds which are contained in the given set of ids.
*
* @param ids set of acceptable keyIds
* @return filter
*/
static KeyFilter selected(Collection<Long> ids) {
// noinspection Convert2MethodRef
return keyId -> ids.contains(keyId);
}
}
}

View file

@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Utility classes related to creating keys with GNU DUMMY S2K values.
*/
package org.gnupg;

View file

@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
import org.bouncycastle.bcpg.PacketTags;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
public enum OpenPgpPacket {
PKESK(PacketTags.PUBLIC_KEY_ENC_SESSION),
SIG(PacketTags.SIGNATURE),
SKESK(PacketTags.SYMMETRIC_KEY_ENC_SESSION),
OPS(PacketTags.ONE_PASS_SIGNATURE),
SK(PacketTags.SECRET_KEY),
PK(PacketTags.PUBLIC_KEY),
SSK(PacketTags.SECRET_SUBKEY),
COMP(PacketTags.COMPRESSED_DATA),
SED(PacketTags.SYMMETRIC_KEY_ENC),
MARKER(PacketTags.MARKER),
LIT(PacketTags.LITERAL_DATA),
TRUST(PacketTags.TRUST),
UID(PacketTags.USER_ID),
PSK(PacketTags.PUBLIC_SUBKEY),
UATTR(PacketTags.USER_ATTRIBUTE),
SEIPD(PacketTags.SYM_ENC_INTEGRITY_PRO),
MDC(PacketTags.MOD_DETECTION_CODE),
EXP_1(PacketTags.EXPERIMENTAL_1),
EXP_2(PacketTags.EXPERIMENTAL_2),
EXP_3(PacketTags.EXPERIMENTAL_3),
EXP_4(PacketTags.EXPERIMENTAL_4),
;
static final Map<Integer, OpenPgpPacket> MAP = new HashMap<>();
static {
for (OpenPgpPacket p : OpenPgpPacket.values()) {
MAP.put(p.getTag(), p);
}
}
final int tag;
@Nullable
public static OpenPgpPacket fromTag(int tag) {
return MAP.get(tag);
}
@Nonnull
public static OpenPgpPacket requireFromTag(int tag) {
OpenPgpPacket p = fromTag(tag);
if (p == null) {
throw new NoSuchElementException("No OpenPGP packet known for tag " + tag);
}
return p;
}
OpenPgpPacket(int tag) {
this.tag = tag;
}
int getTag() {
return tag;
}
}

View file

@ -28,7 +28,7 @@ public enum PublicKeyAlgorithm {
/**
* RSA with usage encryption.
*
* @deprecated see https://tools.ietf.org/html/rfc4880#section-13.5
* @deprecated see <a href="https://tools.ietf.org/html/rfc4880#section-13.5">Deprecation notice</a>
*/
@Deprecated
RSA_ENCRYPT (PublicKeyAlgorithmTags.RSA_ENCRYPT, false, true),
@ -36,7 +36,7 @@ public enum PublicKeyAlgorithm {
/**
* RSA with usage of creating signatures.
*
* @deprecated see https://tools.ietf.org/html/rfc4880#section-13.5
* @deprecated see <a href="https://tools.ietf.org/html/rfc4880#section-13.5">Deprecation notice</a>
*/
@Deprecated
RSA_SIGN (PublicKeyAlgorithmTags.RSA_SIGN, true, false),
@ -71,7 +71,7 @@ public enum PublicKeyAlgorithm {
/**
* ElGamal General.
*
* @deprecated see https://tools.ietf.org/html/rfc4880#section-13.8
* @deprecated see <a href="https://tools.ietf.org/html/rfc4880#section-13.8">Deprecation notice</a>
*/
@Deprecated
ELGAMAL_GENERAL (PublicKeyAlgorithmTags.ELGAMAL_GENERAL, true, true),

View file

@ -22,8 +22,10 @@ import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy;
import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.signature.SignatureUtils;
import org.pgpainless.util.Passphrase;
@ -48,6 +50,7 @@ public class ConsumerOptions {
// Session key for decryption without passphrase/key
private SessionKey sessionKey = null;
private final Map<SubkeyIdentifier, PublicKeyDataDecryptorFactory> customPublicKeyDataDecryptorFactories = new HashMap<>();
private final Map<PGPSecretKeyRing, SecretKeyRingProtector> decryptionKeys = new HashMap<>();
private final Set<Passphrase> decryptionPassphrases = new HashSet<>();
@ -238,6 +241,23 @@ public class ConsumerOptions {
return this;
}
/**
* Add a custom {@link PublicKeyDataDecryptorFactory} which enable decryption of messages, e.g. using
* hardware-backed secret keys.
* (See e.g. {@link org.pgpainless.decryption_verification.HardwareSecurity.HardwareDataDecryptorFactory}).
*
* @param factory decryptor factory
* @return options
*/
public ConsumerOptions addCustomDecryptorFactory(@Nonnull CustomPublicKeyDataDecryptorFactory factory) {
this.customPublicKeyDataDecryptorFactories.put(factory.getSubkeyIdentifier(), factory);
return this;
}
Map<SubkeyIdentifier, PublicKeyDataDecryptorFactory> getCustomDecryptorFactories() {
return new HashMap<>(customPublicKeyDataDecryptorFactories);
}
public @Nonnull Set<PGPSecretKeyRing> getDecryptionKeys() {
return Collections.unmodifiableSet(decryptionKeys.keySet());
}

View file

@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
import org.pgpainless.key.SubkeyIdentifier;
public interface CustomPublicKeyDataDecryptorFactory extends PublicKeyDataDecryptorFactory {
SubkeyIdentifier getSubkeyIdentifier();
}

View file

@ -31,7 +31,7 @@ public class DecryptionBuilder implements DecryptionBuilderInterface {
throw new IllegalArgumentException("Consumer options cannot be null.");
}
return DecryptionStreamFactory.create(inputStream, consumerOptions);
return OpenPgpMessageInputStream.create(inputStream, consumerOptions);
}
}
}

View file

@ -1,65 +1,13 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import java.io.IOException;
import java.io.InputStream;
import javax.annotation.Nonnull;
import org.bouncycastle.util.io.Streams;
/**
* Decryption Stream that handles updating and verification of detached signatures,
* as well as verification of integrity-protected input streams once the stream gets closed.
*/
public class DecryptionStream extends CloseForResultInputStream {
private final InputStream inputStream;
private final IntegrityProtectedInputStream integrityProtectedInputStream;
private final InputStream armorStream;
/**
* Create an input stream that handles decryption and - if necessary - integrity protection verification.
*
* @param wrapped underlying input stream
* @param resultBuilder builder for decryption metadata like algorithms, recipients etc.
* @param integrityProtectedInputStream in case of data encrypted using SEIP packet close this stream to check integrity
* @param armorStream armor stream to verify CRC checksums
*/
DecryptionStream(@Nonnull InputStream wrapped,
@Nonnull OpenPgpMetadata.Builder resultBuilder,
IntegrityProtectedInputStream integrityProtectedInputStream,
InputStream armorStream) {
public abstract class DecryptionStream extends CloseForResultInputStream {
public DecryptionStream(@Nonnull OpenPgpMetadata.Builder resultBuilder) {
super(resultBuilder);
this.inputStream = wrapped;
this.integrityProtectedInputStream = integrityProtectedInputStream;
this.armorStream = armorStream;
}
@Override
public void close() throws IOException {
if (armorStream != null) {
Streams.drain(armorStream);
}
inputStream.close();
if (integrityProtectedInputStream != null) {
integrityProtectedInputStream.close();
}
super.close();
}
@Override
public int read() throws IOException {
int r = inputStream.read();
return r;
}
@Override
public int read(@Nonnull byte[] bytes, int offset, int length) throws IOException {
int read = inputStream.read(bytes, offset, length);
return read;
}
}

View file

@ -1,650 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.bouncycastle.bcpg.ArmoredInputStream;
import org.bouncycastle.openpgp.PGPCompressedData;
import org.bouncycastle.openpgp.PGPEncryptedData;
import org.bouncycastle.openpgp.PGPEncryptedDataList;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPOnePassSignature;
import org.bouncycastle.openpgp.PGPOnePassSignatureList;
import org.bouncycastle.openpgp.PGPPBEEncryptedData;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSessionKey;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureList;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory;
import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider;
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.CompressionAlgorithm;
import org.pgpainless.algorithm.EncryptionPurpose;
import org.pgpainless.algorithm.StreamEncoding;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil;
import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy;
import org.pgpainless.exception.FinalIOException;
import org.pgpainless.exception.MessageNotIntegrityProtectedException;
import org.pgpainless.exception.MissingDecryptionMethodException;
import org.pgpainless.exception.MissingLiteralDataException;
import org.pgpainless.exception.MissingPassphraseException;
import org.pgpainless.exception.SignatureValidationException;
import org.pgpainless.exception.UnacceptableAlgorithmException;
import org.pgpainless.implementation.ImplementationFactory;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.key.info.KeyRingInfo;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.key.protection.UnlockSecretKey;
import org.pgpainless.signature.SignatureUtils;
import org.pgpainless.signature.consumer.DetachedSignatureCheck;
import org.pgpainless.signature.consumer.OnePassSignatureCheck;
import org.pgpainless.util.ArmoredInputStreamFactory;
import org.pgpainless.util.Passphrase;
import org.pgpainless.util.SessionKey;
import org.pgpainless.util.Tuple;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class DecryptionStreamFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(DecryptionStreamFactory.class);
// Maximum nesting depth of packets (e.g. compression, encryption...)
private static final int MAX_PACKET_NESTING_DEPTH = 16;
private final ConsumerOptions options;
private final OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder();
private final List<OnePassSignatureCheck> onePassSignatureChecks = new ArrayList<>();
private final List<DetachedSignatureCheck> detachedSignatureChecks = new ArrayList<>();
private final Map<Long, OnePassSignatureCheck> onePassSignaturesWithMissingCert = new HashMap<>();
private static final PGPContentVerifierBuilderProvider verifierBuilderProvider =
ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider();
private IntegrityProtectedInputStream integrityProtectedEncryptedInputStream;
public static DecryptionStream create(@Nonnull InputStream inputStream,
@Nonnull ConsumerOptions options)
throws PGPException, IOException {
DecryptionStreamFactory factory = new DecryptionStreamFactory(options);
OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream);
return factory.parseOpenPGPDataAndCreateDecryptionStream(openPgpIn);
}
public DecryptionStreamFactory(ConsumerOptions options) {
this.options = options;
initializeDetachedSignatures(options.getDetachedSignatures());
}
private void initializeDetachedSignatures(Set<PGPSignature> signatures) {
for (PGPSignature signature : signatures) {
long issuerKeyId = SignatureUtils.determineIssuerKeyId(signature);
PGPPublicKeyRing signingKeyRing = findSignatureVerificationKeyRing(issuerKeyId);
if (signingKeyRing == null) {
SignatureValidationException ex = new SignatureValidationException(
"Missing verification certificate " + Long.toHexString(issuerKeyId));
resultBuilder.addInvalidDetachedSignature(new SignatureVerification(signature, null), ex);
continue;
}
PGPPublicKey signingKey = signingKeyRing.getPublicKey(issuerKeyId);
SubkeyIdentifier signingKeyIdentifier = new SubkeyIdentifier(signingKeyRing, signingKey.getKeyID());
try {
signature.init(verifierBuilderProvider, signingKey);
DetachedSignatureCheck detachedSignature =
new DetachedSignatureCheck(signature, signingKeyRing, signingKeyIdentifier);
detachedSignatureChecks.add(detachedSignature);
} catch (PGPException e) {
SignatureValidationException ex = new SignatureValidationException(
"Cannot verify detached signature made by " + signingKeyIdentifier + ".", e);
resultBuilder.addInvalidDetachedSignature(new SignatureVerification(signature, signingKeyIdentifier), ex);
}
}
}
private DecryptionStream parseOpenPGPDataAndCreateDecryptionStream(OpenPgpInputStream openPgpIn)
throws IOException, PGPException {
InputStream pgpInStream;
InputStream outerDecodingStream;
PGPObjectFactory objectFactory;
// Non-OpenPGP data. We are probably verifying detached signatures
if (openPgpIn.isNonOpenPgp() || options.isForceNonOpenPgpData()) {
outerDecodingStream = openPgpIn;
pgpInStream = wrapInVerifySignatureStream(outerDecodingStream, null);
return new DecryptionStream(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream, null);
}
// Data appears to be OpenPGP message,
// or we handle it as such, since user provided a session-key for decryption
if (openPgpIn.isLikelyOpenPgpMessage() ||
(openPgpIn.isBinaryOpenPgp() && options.getSessionKey() != null)) {
outerDecodingStream = openPgpIn;
objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream);
// Parse OpenPGP message
pgpInStream = processPGPPackets(objectFactory, 1);
return new DecryptionStream(pgpInStream,
resultBuilder, integrityProtectedEncryptedInputStream, null);
}
if (openPgpIn.isAsciiArmored()) {
ArmoredInputStream armoredInputStream = ArmoredInputStreamFactory.get(openPgpIn);
if (armoredInputStream.isClearText()) {
resultBuilder.setCleartextSigned();
return parseCleartextSignedMessage(armoredInputStream);
} else {
outerDecodingStream = armoredInputStream;
objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream);
// Parse OpenPGP message
pgpInStream = processPGPPackets(objectFactory, 1);
return new DecryptionStream(pgpInStream,
resultBuilder, integrityProtectedEncryptedInputStream,
outerDecodingStream);
}
}
throw new PGPException("Not sure how to handle the input stream.");
}
private DecryptionStream parseCleartextSignedMessage(ArmoredInputStream armorIn)
throws IOException, PGPException {
resultBuilder.setCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)
.setFileEncoding(StreamEncoding.TEXT);
MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy();
PGPSignatureList signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(armorIn, multiPassStrategy.getMessageOutputStream());
for (PGPSignature signature : signatures) {
options.addVerificationOfDetachedSignature(signature);
}
initializeDetachedSignatures(options.getDetachedSignatures());
InputStream verifyIn = wrapInVerifySignatureStream(multiPassStrategy.getMessageInputStream(), null);
return new DecryptionStream(verifyIn, resultBuilder, integrityProtectedEncryptedInputStream,
null);
}
private InputStream wrapInVerifySignatureStream(InputStream bufferedIn, @Nullable PGPObjectFactory objectFactory) {
return new SignatureInputStream.VerifySignatures(
bufferedIn, objectFactory, onePassSignatureChecks,
onePassSignaturesWithMissingCert, detachedSignatureChecks, options,
resultBuilder);
}
private InputStream processPGPPackets(@Nonnull PGPObjectFactory objectFactory, int depth)
throws IOException, PGPException {
if (depth >= MAX_PACKET_NESTING_DEPTH) {
throw new PGPException("Maximum depth of nested packages exceeded.");
}
Object nextPgpObject;
try {
while ((nextPgpObject = objectFactory.nextObject()) != null) {
if (nextPgpObject instanceof PGPEncryptedDataList) {
return processPGPEncryptedDataList((PGPEncryptedDataList) nextPgpObject, depth);
}
if (nextPgpObject instanceof PGPCompressedData) {
return processPGPCompressedData((PGPCompressedData) nextPgpObject, depth);
}
if (nextPgpObject instanceof PGPOnePassSignatureList) {
return processOnePassSignatureList(objectFactory, (PGPOnePassSignatureList) nextPgpObject, depth);
}
if (nextPgpObject instanceof PGPLiteralData) {
return processPGPLiteralData(objectFactory, (PGPLiteralData) nextPgpObject, depth);
}
}
} catch (FinalIOException e) {
throw e;
} catch (IOException e) {
if (depth == 1 && e.getMessage().contains("invalid armor")) {
throw e;
}
if (depth == 1 && e.getMessage().contains("unknown object in stream:")) {
throw new MissingLiteralDataException("No Literal Data Packet found.");
} else {
throw new FinalIOException(e);
}
}
throw new MissingLiteralDataException("No Literal Data Packet found");
}
private InputStream processPGPEncryptedDataList(PGPEncryptedDataList pgpEncryptedDataList, int depth)
throws PGPException, IOException {
LOGGER.debug("Depth {}: Encountered PGPEncryptedDataList", depth);
SessionKey sessionKey = options.getSessionKey();
if (sessionKey != null) {
integrityProtectedEncryptedInputStream = decryptWithProvidedSessionKey(pgpEncryptedDataList, sessionKey);
InputStream decodedDataStream = PGPUtil.getDecoderStream(integrityProtectedEncryptedInputStream);
PGPObjectFactory factory = ImplementationFactory.getInstance().getPGPObjectFactory(decodedDataStream);
return processPGPPackets(factory, ++depth);
}
InputStream decryptedDataStream = decryptSessionKey(pgpEncryptedDataList);
InputStream decodedDataStream = PGPUtil.getDecoderStream(decryptedDataStream);
PGPObjectFactory factory = ImplementationFactory.getInstance().getPGPObjectFactory(decodedDataStream);
return processPGPPackets(factory, ++depth);
}
private IntegrityProtectedInputStream decryptWithProvidedSessionKey(
PGPEncryptedDataList pgpEncryptedDataList,
SessionKey sessionKey)
throws PGPException {
SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance()
.getSessionKeyDataDecryptorFactory(sessionKey);
InputStream decryptedDataStream = null;
PGPEncryptedData encryptedData = null;
for (PGPEncryptedData pgpEncryptedData : pgpEncryptedDataList) {
encryptedData = pgpEncryptedData;
if (!options.isIgnoreMDCErrors() && !encryptedData.isIntegrityProtected()) {
throw new MessageNotIntegrityProtectedException();
}
if (encryptedData instanceof PGPPBEEncryptedData) {
PGPPBEEncryptedData pbeEncrypted = (PGPPBEEncryptedData) encryptedData;
decryptedDataStream = pbeEncrypted.getDataStream(decryptorFactory);
break;
} else if (encryptedData instanceof PGPPublicKeyEncryptedData) {
PGPPublicKeyEncryptedData pkEncrypted = (PGPPublicKeyEncryptedData) encryptedData;
decryptedDataStream = pkEncrypted.getDataStream(decryptorFactory);
break;
}
}
if (decryptedDataStream == null) {
throw new PGPException("No valid PGP data encountered.");
}
resultBuilder.setSessionKey(sessionKey);
throwIfAlgorithmIsRejected(sessionKey.getAlgorithm());
integrityProtectedEncryptedInputStream =
new IntegrityProtectedInputStream(decryptedDataStream, encryptedData, options);
return integrityProtectedEncryptedInputStream;
}
private InputStream processPGPCompressedData(PGPCompressedData pgpCompressedData, int depth)
throws PGPException, IOException {
try {
CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.requireFromId(pgpCompressedData.getAlgorithm());
LOGGER.debug("Depth {}: Encountered PGPCompressedData: {}", depth, compressionAlgorithm);
resultBuilder.setCompressionAlgorithm(compressionAlgorithm);
} catch (NoSuchElementException e) {
throw new PGPException("Unknown compression algorithm encountered.", e);
}
InputStream inflatedDataStream = pgpCompressedData.getDataStream();
InputStream decodedDataStream = PGPUtil.getDecoderStream(inflatedDataStream);
PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decodedDataStream);
return processPGPPackets(objectFactory, ++depth);
}
private InputStream processOnePassSignatureList(
@Nonnull PGPObjectFactory objectFactory,
PGPOnePassSignatureList onePassSignatures,
int depth)
throws PGPException, IOException {
LOGGER.debug("Depth {}: Encountered PGPOnePassSignatureList of size {}", depth, onePassSignatures.size());
initOnePassSignatures(onePassSignatures);
return processPGPPackets(objectFactory, depth);
}
private InputStream processPGPLiteralData(
@Nonnull PGPObjectFactory objectFactory,
PGPLiteralData pgpLiteralData,
int depth) {
LOGGER.debug("Depth {}: Found PGPLiteralData", depth);
InputStream literalDataInputStream = pgpLiteralData.getInputStream();
resultBuilder.setFileName(pgpLiteralData.getFileName())
.setModificationDate(pgpLiteralData.getModificationTime())
.setFileEncoding(StreamEncoding.requireFromCode(pgpLiteralData.getFormat()));
if (onePassSignatureChecks.isEmpty() && onePassSignaturesWithMissingCert.isEmpty()) {
LOGGER.debug("No OnePassSignatures found -> We are done");
return literalDataInputStream;
}
return new SignatureInputStream.VerifySignatures(literalDataInputStream, objectFactory,
onePassSignatureChecks, onePassSignaturesWithMissingCert, detachedSignatureChecks, options, resultBuilder) {
};
}
private InputStream decryptSessionKey(@Nonnull PGPEncryptedDataList encryptedDataList)
throws PGPException {
Iterator<PGPEncryptedData> encryptedDataIterator = encryptedDataList.getEncryptedDataObjects();
if (!encryptedDataIterator.hasNext()) {
throw new PGPException("Decryption failed - EncryptedDataList has no items");
}
PGPPrivateKey decryptionKey = null;
PGPPublicKeyEncryptedData encryptedSessionKey = null;
List<PGPPBEEncryptedData> passphraseProtected = new ArrayList<>();
List<PGPPublicKeyEncryptedData> publicKeyProtected = new ArrayList<>();
List<Tuple<SubkeyIdentifier, PGPPublicKeyEncryptedData>> postponedDueToMissingPassphrase = new ArrayList<>();
// Sort PKESK and SKESK packets
while (encryptedDataIterator.hasNext()) {
PGPEncryptedData encryptedData = encryptedDataIterator.next();
if (!encryptedData.isIntegrityProtected() && !options.isIgnoreMDCErrors()) {
throw new MessageNotIntegrityProtectedException();
}
// SKESK
if (encryptedData instanceof PGPPBEEncryptedData) {
passphraseProtected.add((PGPPBEEncryptedData) encryptedData);
}
// PKESK
else if (encryptedData instanceof PGPPublicKeyEncryptedData) {
publicKeyProtected.add((PGPPublicKeyEncryptedData) encryptedData);
}
}
// Try decryption with passphrases first
for (PGPPBEEncryptedData pbeEncryptedData : passphraseProtected) {
for (Passphrase passphrase : options.getDecryptionPassphrases()) {
PBEDataDecryptorFactory passphraseDecryptor = ImplementationFactory.getInstance()
.getPBEDataDecryptorFactory(passphrase);
try {
InputStream decryptedDataStream = pbeEncryptedData.getDataStream(passphraseDecryptor);
PGPSessionKey pgpSessionKey = pbeEncryptedData.getSessionKey(passphraseDecryptor);
SessionKey sessionKey = new SessionKey(pgpSessionKey);
resultBuilder.setSessionKey(sessionKey);
throwIfAlgorithmIsRejected(sessionKey.getAlgorithm());
integrityProtectedEncryptedInputStream =
new IntegrityProtectedInputStream(decryptedDataStream, pbeEncryptedData, options);
return integrityProtectedEncryptedInputStream;
} catch (PGPException e) {
LOGGER.debug("Probable passphrase mismatch, skip PBE encrypted data block", e);
}
}
}
// Then try decryption with public key encryption
for (PGPPublicKeyEncryptedData publicKeyEncryptedData : publicKeyProtected) {
PGPPrivateKey privateKey = null;
if (options.getDecryptionKeys().isEmpty()) {
break;
}
long keyId = publicKeyEncryptedData.getKeyID();
// Wildcard KeyID
if (keyId == 0L) {
LOGGER.debug("Hidden recipient detected. Try to decrypt with all available secret keys.");
for (PGPSecretKeyRing secretKeys : options.getDecryptionKeys()) {
if (privateKey != null) {
break;
}
KeyRingInfo info = new KeyRingInfo(secretKeys);
List<PGPPublicKey> encryptionSubkeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY);
for (PGPPublicKey pubkey : encryptionSubkeys) {
PGPSecretKey secretKey = secretKeys.getSecretKey(pubkey.getKeyID());
// Skip missing secret key
if (secretKey == null) {
continue;
}
privateKey = tryPublicKeyDecryption(secretKeys, secretKey, publicKeyEncryptedData,
postponedDueToMissingPassphrase, true);
}
}
}
// Non-wildcard key-id
else {
LOGGER.debug("PGPEncryptedData is encrypted for key {}", Long.toHexString(keyId));
resultBuilder.addRecipientKeyId(keyId);
PGPSecretKeyRing secretKeys = findDecryptionKeyRing(keyId);
if (secretKeys == null) {
LOGGER.debug("Missing certificate of {}. Skip.", Long.toHexString(keyId));
continue;
}
// Make sure that the recipient key is encryption capable and non-expired
KeyRingInfo info = new KeyRingInfo(secretKeys);
List<PGPPublicKey> encryptionSubkeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY);
PGPSecretKey secretKey = null;
for (PGPPublicKey pubkey : encryptionSubkeys) {
if (pubkey.getKeyID() == keyId) {
secretKey = secretKeys.getSecretKey(keyId);
break;
}
}
if (secretKey == null) {
LOGGER.debug("Key " + Long.toHexString(keyId) + " is not valid or not capable for decryption.");
} else {
privateKey = tryPublicKeyDecryption(secretKeys, secretKey, publicKeyEncryptedData,
postponedDueToMissingPassphrase, true);
}
}
if (privateKey == null) {
continue;
}
decryptionKey = privateKey;
encryptedSessionKey = publicKeyEncryptedData;
break;
}
// Try postponed keys with missing passphrases (will cause missing passphrase callbacks to fire)
if (encryptedSessionKey == null) {
if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.THROW_EXCEPTION) {
// Non-interactive mode: Throw an exception with all locked decryption keys
Set<SubkeyIdentifier> keyIds = new HashSet<>();
for (Tuple<SubkeyIdentifier, ?> k : postponedDueToMissingPassphrase) {
keyIds.add(k.getA());
}
if (!keyIds.isEmpty()) {
throw new MissingPassphraseException(keyIds);
}
}
else if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.INTERACTIVE) {
// Interactive mode: Fire protector callbacks to get passphrases interactively
for (Tuple<SubkeyIdentifier, PGPPublicKeyEncryptedData> missingPassphrases : postponedDueToMissingPassphrase) {
SubkeyIdentifier keyId = missingPassphrases.getA();
PGPPublicKeyEncryptedData publicKeyEncryptedData = missingPassphrases.getB();
PGPSecretKeyRing secretKeys = findDecryptionKeyRing(keyId.getKeyId());
PGPSecretKey secretKey = secretKeys.getSecretKey(keyId.getSubkeyId());
PGPPrivateKey privateKey = tryPublicKeyDecryption(secretKeys, secretKey, publicKeyEncryptedData,
postponedDueToMissingPassphrase, false);
if (privateKey == null) {
continue;
}
decryptionKey = privateKey;
encryptedSessionKey = publicKeyEncryptedData;
break;
}
} else {
throw new IllegalStateException("Invalid PostponedKeysStrategy set in consumer options.");
}
}
return decryptWith(encryptedSessionKey, decryptionKey);
}
/**
* Try decryption of the provided public-key-encrypted-data using the given secret key.
* If the secret key is encrypted and the secret key protector does not have a passphrase available and the boolean
* postponeIfMissingPassphrase is true, data decryption is postponed by pushing a tuple of the encrypted data decryption key
* identifier to the postponed list.
*
* This method only returns a non-null private key, if the private key is able to decrypt the message successfully.
*
* @param secretKeys secret key ring
* @param secretKey secret key
* @param publicKeyEncryptedData encrypted data which is tried to decrypt using the secret key
* @param postponed list of postponed decryptions due to missing secret key passphrases
* @param postponeIfMissingPassphrase flag to specify whether missing secret key passphrases should result in postponed decryption
* @return private key if decryption is successful, null if decryption is unsuccessful or postponed
*
* @throws PGPException in case of an OpenPGP error
*/
private PGPPrivateKey tryPublicKeyDecryption(
PGPSecretKeyRing secretKeys,
PGPSecretKey secretKey,
PGPPublicKeyEncryptedData publicKeyEncryptedData,
List<Tuple<SubkeyIdentifier, PGPPublicKeyEncryptedData>> postponed,
boolean postponeIfMissingPassphrase) throws PGPException {
SecretKeyRingProtector protector = options.getSecretKeyProtector(secretKeys);
if (postponeIfMissingPassphrase && !protector.hasPassphraseFor(secretKey.getKeyID())) {
// Postpone decryption with key with missing passphrase
SubkeyIdentifier identifier = new SubkeyIdentifier(secretKeys, secretKey.getKeyID());
postponed.add(new Tuple<>(identifier, publicKeyEncryptedData));
return null;
}
PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(
secretKey, protector.getDecryptor(secretKey.getKeyID()));
// test if we have the right private key
PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance()
.getPublicKeyDataDecryptorFactory(privateKey);
try {
publicKeyEncryptedData.getSymmetricAlgorithm(decryptorFactory); // will only succeed if we have the right secret key
LOGGER.debug("Found correct decryption key {}.", Long.toHexString(secretKey.getKeyID()));
resultBuilder.setDecryptionKey(new SubkeyIdentifier(secretKeys, privateKey.getKeyID()));
return privateKey;
} catch (PGPException | ClassCastException e) {
return null;
}
}
private InputStream decryptWith(PGPPublicKeyEncryptedData encryptedSessionKey, PGPPrivateKey decryptionKey)
throws PGPException {
if (decryptionKey == null || encryptedSessionKey == null) {
throw new MissingDecryptionMethodException("Decryption failed - No suitable decryption key or passphrase found");
}
PublicKeyDataDecryptorFactory dataDecryptor = ImplementationFactory.getInstance()
.getPublicKeyDataDecryptorFactory(decryptionKey);
PGPSessionKey pgpSessionKey = encryptedSessionKey.getSessionKey(dataDecryptor);
SessionKey sessionKey = new SessionKey(pgpSessionKey);
resultBuilder.setSessionKey(sessionKey);
SymmetricKeyAlgorithm symmetricKeyAlgorithm = sessionKey.getAlgorithm();
if (symmetricKeyAlgorithm == SymmetricKeyAlgorithm.NULL) {
LOGGER.debug("Message is unencrypted");
} else {
LOGGER.debug("Message is encrypted using {}", symmetricKeyAlgorithm);
}
throwIfAlgorithmIsRejected(symmetricKeyAlgorithm);
integrityProtectedEncryptedInputStream = new IntegrityProtectedInputStream(
encryptedSessionKey.getDataStream(dataDecryptor), encryptedSessionKey, options);
return integrityProtectedEncryptedInputStream;
}
private void throwIfAlgorithmIsRejected(SymmetricKeyAlgorithm algorithm)
throws UnacceptableAlgorithmException {
if (!PGPainless.getPolicy().getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(algorithm)) {
throw new UnacceptableAlgorithmException("Data is "
+ (algorithm == SymmetricKeyAlgorithm.NULL ?
"unencrypted" :
"encrypted with symmetric algorithm " + algorithm) + " which is not acceptable as per PGPainless' policy.\n" +
"To mark this algorithm as acceptable, use PGPainless.getPolicy().setSymmetricKeyDecryptionAlgorithmPolicy().");
}
}
private void initOnePassSignatures(@Nonnull PGPOnePassSignatureList onePassSignatureList)
throws PGPException {
Iterator<PGPOnePassSignature> iterator = onePassSignatureList.iterator();
if (!iterator.hasNext()) {
throw new PGPException("Verification failed - No OnePassSignatures found");
}
processOnePassSignatures(iterator);
}
private void processOnePassSignatures(Iterator<PGPOnePassSignature> signatures)
throws PGPException {
while (signatures.hasNext()) {
PGPOnePassSignature signature = signatures.next();
processOnePassSignature(signature);
}
}
private void processOnePassSignature(PGPOnePassSignature signature)
throws PGPException {
final long keyId = signature.getKeyID();
LOGGER.debug("Encountered OnePassSignature from {}", Long.toHexString(keyId));
// Find public key
PGPPublicKeyRing verificationKeyRing = findSignatureVerificationKeyRing(keyId);
if (verificationKeyRing == null) {
onePassSignaturesWithMissingCert.put(keyId, new OnePassSignatureCheck(signature, null));
return;
}
PGPPublicKey verificationKey = verificationKeyRing.getPublicKey(keyId);
signature.init(verifierBuilderProvider, verificationKey);
OnePassSignatureCheck onePassSignature = new OnePassSignatureCheck(signature, verificationKeyRing);
onePassSignatureChecks.add(onePassSignature);
}
private PGPSecretKeyRing findDecryptionKeyRing(long keyId) {
for (PGPSecretKeyRing key : options.getDecryptionKeys()) {
if (key.getSecretKey(keyId) != null) {
return key;
}
}
return null;
}
private PGPPublicKeyRing findSignatureVerificationKeyRing(long keyId) {
PGPPublicKeyRing verificationKeyRing = null;
for (PGPPublicKeyRing publicKeyRing : options.getCertificates()) {
PGPPublicKey verificationKey = publicKeyRing.getPublicKey(keyId);
if (verificationKey != null) {
LOGGER.debug("Found public key {} for signature verification", Long.toHexString(keyId));
verificationKeyRing = publicKeyRing;
break;
}
}
if (verificationKeyRing == null && options.getMissingCertificateCallback() != null) {
verificationKeyRing = options.getMissingCertificateCallback().onMissingPublicKeyEncountered(keyId);
}
return verificationKeyRing;
}
}

View file

@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.operator.PGPDataDecryptor;
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory;
import org.pgpainless.key.SubkeyIdentifier;
/**
* Enable integration of hardware-backed OpenPGP keys.
*/
public class HardwareSecurity {
public interface DecryptionCallback {
/**
* Delegate decryption of a Public-Key-Encrypted-Session-Key (PKESK) to an external API for dealing with
* hardware security modules such as smartcards or TPMs.
*
* If decryption fails for some reason, a subclass of the {@link HardwareSecurityException} is thrown.
*
* @param keyId id of the key
* @param keyAlgorithm algorithm
* @param sessionKeyData encrypted session key
*
* @return decrypted session key
* @throws HardwareSecurityException exception
*/
byte[] decryptSessionKey(long keyId, int keyAlgorithm, byte[] sessionKeyData)
throws HardwareSecurityException;
}
/**
* Implementation of {@link PublicKeyDataDecryptorFactory} which delegates decryption of encrypted session keys
* to a {@link DecryptionCallback}.
* Users can provide such a callback to delegate decryption of messages to hardware security SDKs.
*/
public static class HardwareDataDecryptorFactory implements CustomPublicKeyDataDecryptorFactory {
private final DecryptionCallback callback;
// luckily we can instantiate the BcPublicKeyDataDecryptorFactory with null as argument.
private final PublicKeyDataDecryptorFactory factory =
new BcPublicKeyDataDecryptorFactory(null);
private final SubkeyIdentifier subkey;
/**
* Create a new {@link HardwareDataDecryptorFactory}.
*
* @param callback decryption callback
*/
public HardwareDataDecryptorFactory(SubkeyIdentifier subkeyIdentifier, DecryptionCallback callback) {
this.callback = callback;
this.subkey = subkeyIdentifier;
}
@Override
public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData)
throws PGPException {
try {
// delegate decryption to the callback
return callback.decryptSessionKey(subkey.getSubkeyId(), keyAlgorithm, secKeyData[0]);
} catch (HardwareSecurityException e) {
throw new PGPException("Hardware-backed decryption failed.", e);
}
}
@Override
public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key)
throws PGPException {
return factory.createDataDecryptor(withIntegrityPacket, encAlgorithm, key);
}
@Override
public PGPDataDecryptor createDataDecryptor(int aeadAlgorithm, byte[] iv, int chunkSize, int encAlgorithm, byte[] key)
throws PGPException {
return factory.createDataDecryptor(aeadAlgorithm, iv, chunkSize, encAlgorithm, key);
}
@Override
public SubkeyIdentifier getSubkeyIdentifier() {
return subkey;
}
}
public static class HardwareSecurityException
extends Exception {
}
}

View file

@ -11,12 +11,17 @@ import javax.annotation.Nonnull;
import org.bouncycastle.openpgp.PGPEncryptedData;
import org.bouncycastle.openpgp.PGPException;
import org.pgpainless.exception.ModificationDetectionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class IntegrityProtectedInputStream extends InputStream {
private static final Logger LOGGER = LoggerFactory.getLogger(IntegrityProtectedInputStream.class);
private final InputStream inputStream;
private final PGPEncryptedData encryptedData;
private final ConsumerOptions options;
private boolean closed = false;
public IntegrityProtectedInputStream(InputStream inputStream, PGPEncryptedData encryptedData, ConsumerOptions options) {
this.inputStream = inputStream;
@ -36,11 +41,17 @@ public class IntegrityProtectedInputStream extends InputStream {
@Override
public void close() throws IOException {
if (closed) {
return;
}
closed = true;
if (encryptedData.isIntegrityProtected() && !options.isIgnoreMDCErrors()) {
try {
if (!encryptedData.verify()) {
throw new ModificationDetectionException();
}
LOGGER.debug("Integrity Protection check passed");
} catch (PGPException e) {
throw new IOException("Failed to verify integrity protection", e);
}

View file

@ -0,0 +1,456 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import org.pgpainless.algorithm.CompressionAlgorithm;
import org.pgpainless.algorithm.StreamEncoding;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.exception.MalformedOpenPgpMessageException;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.util.SessionKey;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
public class MessageMetadata {
protected Message message;
public MessageMetadata(@Nonnull Message message) {
this.message = message;
}
public @Nullable SymmetricKeyAlgorithm getEncryptionAlgorithm() {
Iterator<SymmetricKeyAlgorithm> algorithms = getEncryptionAlgorithms();
if (algorithms.hasNext()) {
return algorithms.next();
}
return null;
}
public @Nonnull Iterator<SymmetricKeyAlgorithm> getEncryptionAlgorithms() {
return new LayerIterator<SymmetricKeyAlgorithm>(message) {
@Override
public boolean matches(Nested layer) {
return layer instanceof EncryptedData;
}
@Override
public SymmetricKeyAlgorithm getProperty(Layer last) {
return ((EncryptedData) last).algorithm;
}
};
}
public @Nullable CompressionAlgorithm getCompressionAlgorithm() {
Iterator<CompressionAlgorithm> algorithms = getCompressionAlgorithms();
if (algorithms.hasNext()) {
return algorithms.next();
}
return null;
}
public @Nonnull Iterator<CompressionAlgorithm> getCompressionAlgorithms() {
return new LayerIterator<CompressionAlgorithm>(message) {
@Override
public boolean matches(Nested layer) {
return layer instanceof CompressedData;
}
@Override
public CompressionAlgorithm getProperty(Layer last) {
return ((CompressedData) last).algorithm;
}
};
}
public @Nullable SessionKey getSessionKey() {
Iterator<SessionKey> sessionKeys = getSessionKeys();
if (sessionKeys.hasNext()) {
return sessionKeys.next();
}
return null;
}
public @Nonnull Iterator<SessionKey> getSessionKeys() {
return new LayerIterator<SessionKey>(message) {
@Override
boolean matches(Nested layer) {
return layer instanceof EncryptedData;
}
@Override
SessionKey getProperty(Layer last) {
return ((EncryptedData) last).getSessionKey();
}
};
}
public List<SignatureVerification> getVerifiedDetachedSignatures() {
return new ArrayList<>(message.verifiedDetachedSignatures);
}
public List<SignatureVerification.Failure> getRejectedDetachedSignatures() {
return new ArrayList<>(message.rejectedDetachedSignatures);
}
public @Nonnull List<SignatureVerification> getVerifiedInlineSignatures() {
List<SignatureVerification> verifications = new ArrayList<>();
Iterator<List<SignatureVerification>> verificationsByLayer = getVerifiedInlineSignaturesByLayer();
while (verificationsByLayer.hasNext()) {
verifications.addAll(verificationsByLayer.next());
}
return verifications;
}
public @Nonnull Iterator<List<SignatureVerification>> getVerifiedInlineSignaturesByLayer() {
return new LayerIterator<List<SignatureVerification>>(message) {
@Override
boolean matches(Nested layer) {
return layer instanceof Layer;
}
@Override
boolean matches(Layer layer) {
return true;
}
@Override
List<SignatureVerification> getProperty(Layer last) {
List<SignatureVerification> list = new ArrayList<>();
list.addAll(last.getVerifiedOnePassSignatures());
list.addAll(last.getVerifiedPrependedSignatures());
return list;
}
};
}
public @Nonnull List<SignatureVerification.Failure> getRejectedInlineSignatures() {
List<SignatureVerification.Failure> rejected = new ArrayList<>();
Iterator<List<SignatureVerification.Failure>> rejectedByLayer = getRejectedInlineSignaturesByLayer();
while (rejectedByLayer.hasNext()) {
rejected.addAll(rejectedByLayer.next());
}
return rejected;
}
public @Nonnull Iterator<List<SignatureVerification.Failure>> getRejectedInlineSignaturesByLayer() {
return new LayerIterator<List<SignatureVerification.Failure>>(message) {
@Override
boolean matches(Nested layer) {
return layer instanceof Layer;
}
@Override
boolean matches(Layer layer) {
return true;
}
@Override
List<SignatureVerification.Failure> getProperty(Layer last) {
List<SignatureVerification.Failure> list = new ArrayList<>();
list.addAll(last.getRejectedOnePassSignatures());
list.addAll(last.getRejectedPrependedSignatures());
return list;
}
};
}
public String getFilename() {
LiteralData literalData = findLiteralData();
if (literalData == null) {
return null;
}
return literalData.getFileName();
}
public Date getModificationDate() {
LiteralData literalData = findLiteralData();
if (literalData == null) {
return null;
}
return literalData.getModificationDate();
}
public StreamEncoding getFormat() {
LiteralData literalData = findLiteralData();
if (literalData == null) {
return null;
}
return literalData.getFormat();
}
private LiteralData findLiteralData() {
Nested nested = message.child;
if (nested == null) {
return null;
}
while (nested.hasNestedChild()) {
Layer layer = (Layer) nested;
nested = layer.child;
}
return (LiteralData) nested;
}
public SubkeyIdentifier getDecryptionKey() {
Iterator<SubkeyIdentifier> iterator = new LayerIterator<SubkeyIdentifier>(message) {
@Override
public boolean matches(Nested layer) {
return layer instanceof EncryptedData;
}
@Override
public SubkeyIdentifier getProperty(Layer last) {
return ((EncryptedData) last).decryptionKey;
}
};
if (iterator.hasNext()) {
return iterator.next();
}
return null;
}
public abstract static class Layer {
public static final int MAX_LAYER_DEPTH = 16;
protected final int depth;
protected final List<SignatureVerification> verifiedDetachedSignatures = new ArrayList<>();
protected final List<SignatureVerification.Failure> rejectedDetachedSignatures = new ArrayList<>();
protected final List<SignatureVerification> verifiedOnePassSignatures = new ArrayList<>();
protected final List<SignatureVerification.Failure> rejectedOnePassSignatures = new ArrayList<>();
protected final List<SignatureVerification> verifiedPrependedSignatures = new ArrayList<>();
protected final List<SignatureVerification.Failure> rejectedPrependedSignatures = new ArrayList<>();
protected Nested child;
public Layer(int depth) {
this.depth = depth;
if (depth > MAX_LAYER_DEPTH) {
throw new MalformedOpenPgpMessageException("Maximum nesting depth exceeded.");
}
}
public Nested getChild() {
return child;
}
public void setChild(Nested child) {
this.child = child;
}
public List<SignatureVerification> getVerifiedDetachedSignatures() {
return new ArrayList<>(verifiedDetachedSignatures);
}
public List<SignatureVerification.Failure> getRejectedDetachedSignatures() {
return new ArrayList<>(rejectedDetachedSignatures);
}
void addVerifiedDetachedSignature(SignatureVerification signatureVerification) {
verifiedDetachedSignatures.add(signatureVerification);
}
void addRejectedDetachedSignature(SignatureVerification.Failure failure) {
rejectedDetachedSignatures.add(failure);
}
public List<SignatureVerification> getVerifiedOnePassSignatures() {
return new ArrayList<>(verifiedOnePassSignatures);
}
public List<SignatureVerification.Failure> getRejectedOnePassSignatures() {
return new ArrayList<>(rejectedOnePassSignatures);
}
void addVerifiedOnePassSignature(SignatureVerification verifiedOnePassSignature) {
this.verifiedOnePassSignatures.add(verifiedOnePassSignature);
}
void addRejectedOnePassSignature(SignatureVerification.Failure rejected) {
this.rejectedOnePassSignatures.add(rejected);
}
public List<SignatureVerification> getVerifiedPrependedSignatures() {
return new ArrayList<>(verifiedPrependedSignatures);
}
public List<SignatureVerification.Failure> getRejectedPrependedSignatures() {
return new ArrayList<>(rejectedPrependedSignatures);
}
void addVerifiedPrependedSignature(SignatureVerification verified) {
this.verifiedPrependedSignatures.add(verified);
}
void addRejectedPrependedSignature(SignatureVerification.Failure rejected) {
this.rejectedPrependedSignatures.add(rejected);
}
}
public interface Nested {
boolean hasNestedChild();
}
public static class Message extends Layer {
public Message() {
super(0);
}
}
public static class LiteralData implements Nested {
protected String fileName;
protected Date modificationDate;
protected StreamEncoding format;
public LiteralData() {
this("", new Date(0L), StreamEncoding.BINARY);
}
public LiteralData(String fileName, Date modificationDate, StreamEncoding format) {
this.fileName = fileName;
this.modificationDate = modificationDate;
this.format = format;
}
public String getFileName() {
return fileName;
}
public Date getModificationDate() {
return modificationDate;
}
public StreamEncoding getFormat() {
return format;
}
@Override
public boolean hasNestedChild() {
return false;
}
}
public static class CompressedData extends Layer implements Nested {
protected final CompressionAlgorithm algorithm;
public CompressedData(CompressionAlgorithm zip, int depth) {
super(depth);
this.algorithm = zip;
}
public CompressionAlgorithm getAlgorithm() {
return algorithm;
}
@Override
public boolean hasNestedChild() {
return true;
}
}
public static class EncryptedData extends Layer implements Nested {
protected final SymmetricKeyAlgorithm algorithm;
protected SubkeyIdentifier decryptionKey;
protected SessionKey sessionKey;
protected List<Long> recipients;
public EncryptedData(SymmetricKeyAlgorithm algorithm, int depth) {
super(depth);
this.algorithm = algorithm;
}
public SymmetricKeyAlgorithm getAlgorithm() {
return algorithm;
}
public SessionKey getSessionKey() {
return sessionKey;
}
public List<Long> getRecipients() {
return new ArrayList<>(recipients);
}
@Override
public boolean hasNestedChild() {
return true;
}
}
private abstract static class LayerIterator<O> implements Iterator<O> {
private Nested current;
Layer last = null;
Message parent;
LayerIterator(Message message) {
super();
this.parent = message;
this.current = message.child;
if (matches(current)) {
last = (Layer) current;
}
}
@Override
public boolean hasNext() {
if (parent != null && matches(parent)) {
return true;
}
if (last == null) {
findNext();
}
return last != null;
}
@Override
public O next() {
if (parent != null && matches(parent)) {
O property = getProperty(parent);
parent = null;
return property;
}
if (last == null) {
findNext();
}
if (last != null) {
O property = getProperty(last);
last = null;
return property;
}
throw new NoSuchElementException();
}
private void findNext() {
while (current instanceof Layer) {
current = ((Layer) current).child;
if (matches(current)) {
last = (Layer) current;
break;
}
}
}
boolean matches(Nested layer) {
return false;
}
boolean matches(Layer layer) {
if (layer instanceof Nested) {
return matches((Nested) layer);
}
return false;
}
abstract O getProperty(Layer last);
}
}

View file

@ -20,11 +20,13 @@ public interface MissingPublicKeyCallback {
* you may not only search for the key-id on the key rings primary key!
*
* It would be super cool to provide the OpenPgp fingerprint here, but unfortunately one-pass-signatures
* only contain the key id (see https://datatracker.ietf.org/doc/html/rfc4880#section-5.4)
* only contain the key id.
*
* @param keyId ID of the missing signing (sub)key
*
* @return keyring containing the key or null
*
* @see <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-5.4">RFC</a>
*/
@Nullable PGPPublicKeyRing onMissingPublicKeyEncountered(@Nonnull Long keyId);

View file

@ -47,6 +47,7 @@ import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
public class OpenPgpInputStream extends BufferedInputStream {
@SuppressWarnings("CharsetObjectCanBeUsed")
private static final byte[] ARMOR_HEADER = "-----BEGIN PGP ".getBytes(Charset.forName("UTF8"));
// Buffer beginning bytes of the data

View file

@ -1,215 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import static org.pgpainless.signature.consumer.SignatureValidator.signatureWasCreatedInBounds;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureList;
import org.pgpainless.PGPainless;
import org.pgpainless.exception.SignatureValidationException;
import org.pgpainless.policy.Policy;
import org.pgpainless.signature.consumer.CertificateValidator;
import org.pgpainless.signature.consumer.DetachedSignatureCheck;
import org.pgpainless.signature.consumer.OnePassSignatureCheck;
import org.pgpainless.signature.SignatureUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class SignatureInputStream extends FilterInputStream {
protected SignatureInputStream(InputStream inputStream) {
super(inputStream);
}
public static class VerifySignatures extends SignatureInputStream {
private static final Logger LOGGER = LoggerFactory.getLogger(VerifySignatures.class);
private final PGPObjectFactory objectFactory;
private final List<OnePassSignatureCheck> opSignatures;
private final Map<Long, OnePassSignatureCheck> opSignaturesWithMissingCert;
private final List<DetachedSignatureCheck> detachedSignatures;
private final ConsumerOptions options;
private final OpenPgpMetadata.Builder resultBuilder;
public VerifySignatures(
InputStream literalDataStream,
@Nullable PGPObjectFactory objectFactory,
List<OnePassSignatureCheck> opSignatures,
Map<Long, OnePassSignatureCheck> onePassSignaturesWithMissingCert,
List<DetachedSignatureCheck> detachedSignatures,
ConsumerOptions options,
OpenPgpMetadata.Builder resultBuilder) {
super(literalDataStream);
this.objectFactory = objectFactory;
this.opSignatures = opSignatures;
this.opSignaturesWithMissingCert = onePassSignaturesWithMissingCert;
this.detachedSignatures = detachedSignatures;
this.options = options;
this.resultBuilder = resultBuilder;
}
@Override
public int read() throws IOException {
final int data = super.read();
final boolean endOfStream = data == -1;
if (endOfStream) {
finalizeSignatures();
} else {
byte b = (byte) data;
updateOnePassSignatures(b);
updateDetachedSignatures(b);
}
return data;
}
@Override
public int read(@Nonnull byte[] b, int off, int len) throws IOException {
int read = super.read(b, off, len);
final boolean endOfStream = read == -1;
if (endOfStream) {
finalizeSignatures();
} else {
updateOnePassSignatures(b, off, read);
updateDetachedSignatures(b, off, read);
}
return read;
}
private void finalizeSignatures() {
parseAndCombineSignatures();
verifyOnePassSignatures();
verifyDetachedSignatures();
}
public void parseAndCombineSignatures() {
if (objectFactory == null) {
return;
}
// Parse signatures from message
PGPSignatureList signatures;
try {
signatures = parseSignatures(objectFactory);
} catch (IOException e) {
return;
}
List<PGPSignature> signatureList = SignatureUtils.toList(signatures);
// Set signatures as comparison sigs in OPS checks
for (int i = 0; i < opSignatures.size(); i++) {
int reversedIndex = opSignatures.size() - i - 1;
opSignatures.get(i).setSignature(signatureList.get(reversedIndex));
}
for (PGPSignature signature : signatureList) {
if (opSignaturesWithMissingCert.containsKey(signature.getKeyID())) {
OnePassSignatureCheck check = opSignaturesWithMissingCert.remove(signature.getKeyID());
check.setSignature(signature);
resultBuilder.addInvalidInbandSignature(new SignatureVerification(signature, null),
new SignatureValidationException(
"Missing verification certificate " + Long.toHexString(signature.getKeyID())));
}
}
}
private PGPSignatureList parseSignatures(PGPObjectFactory objectFactory) throws IOException {
PGPSignatureList signatureList = null;
Object pgpObject = objectFactory.nextObject();
while (pgpObject != null && signatureList == null) {
if (pgpObject instanceof PGPSignatureList) {
signatureList = (PGPSignatureList) pgpObject;
} else {
pgpObject = objectFactory.nextObject();
}
}
if (signatureList == null || signatureList.isEmpty()) {
throw new IOException("Verification failed - No Signatures found");
}
return signatureList;
}
private synchronized void verifyOnePassSignatures() {
Policy policy = PGPainless.getPolicy();
for (OnePassSignatureCheck opSignature : opSignatures) {
if (opSignature.getSignature() == null) {
LOGGER.warn("Found OnePassSignature without respective signature packet -> skip");
continue;
}
try {
signatureWasCreatedInBounds(options.getVerifyNotBefore(),
options.getVerifyNotAfter()).verify(opSignature.getSignature());
CertificateValidator.validateCertificateAndVerifyOnePassSignature(opSignature, policy);
resultBuilder.addVerifiedInbandSignature(
new SignatureVerification(opSignature.getSignature(), opSignature.getSigningKey()));
} catch (SignatureValidationException e) {
LOGGER.warn("One-pass-signature verification failed for signature made by key {}: {}",
opSignature.getSigningKey(), e.getMessage(), e);
resultBuilder.addInvalidInbandSignature(
new SignatureVerification(opSignature.getSignature(), opSignature.getSigningKey()), e);
}
}
}
private void verifyDetachedSignatures() {
Policy policy = PGPainless.getPolicy();
for (DetachedSignatureCheck s : detachedSignatures) {
try {
signatureWasCreatedInBounds(options.getVerifyNotBefore(),
options.getVerifyNotAfter()).verify(s.getSignature());
CertificateValidator.validateCertificateAndVerifyInitializedSignature(s.getSignature(),
(PGPPublicKeyRing) s.getSigningKeyRing(), policy);
resultBuilder.addVerifiedDetachedSignature(new SignatureVerification(s.getSignature(),
s.getSigningKeyIdentifier()));
} catch (SignatureValidationException e) {
LOGGER.warn("One-pass-signature verification failed for signature made by key {}: {}",
s.getSigningKeyIdentifier(), e.getMessage(), e);
resultBuilder.addInvalidDetachedSignature(new SignatureVerification(s.getSignature(),
s.getSigningKeyIdentifier()), e);
}
}
}
private void updateOnePassSignatures(byte data) {
for (OnePassSignatureCheck opSignature : opSignatures) {
opSignature.getOnePassSignature().update(data);
}
}
private void updateOnePassSignatures(byte[] bytes, int offset, int length) {
for (OnePassSignatureCheck opSignature : opSignatures) {
opSignature.getOnePassSignature().update(bytes, offset, length);
}
}
private void updateDetachedSignatures(byte b) {
for (DetachedSignatureCheck detachedSignature : detachedSignatures) {
detachedSignature.getSignature().update(b);
}
}
private void updateDetachedSignatures(byte[] b, int off, int read) {
for (DetachedSignatureCheck detachedSignature : detachedSignatures) {
detachedSignature.getSignature().update(b, off, read);
}
}
}
}

View file

@ -0,0 +1,164 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.NoSuchElementException;
import org.bouncycastle.bcpg.BCPGInputStream;
import org.bouncycastle.bcpg.MarkerPacket;
import org.bouncycastle.bcpg.Packet;
import org.bouncycastle.openpgp.PGPCompressedData;
import org.bouncycastle.openpgp.PGPEncryptedDataList;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPOnePassSignature;
import org.bouncycastle.openpgp.PGPSignature;
import org.pgpainless.algorithm.OpenPgpPacket;
/**
* Since we need to update signatures with data from the underlying stream, this class is used to tee out the data.
* Unfortunately we cannot simply override {@link BCPGInputStream#read()} to tee the data out though, since
* {@link BCPGInputStream#readPacket()} inconsistently calls a mix of {@link BCPGInputStream#read()} and
* {@link InputStream#read()} of the underlying stream. This would cause the second length byte to get swallowed up.
*
* Therefore, this class delegates the teeing to an {@link DelayedTeeInputStream} which wraps the underlying
* stream. Since calling {@link BCPGInputStream#nextPacketTag()} reads up to and including the next packets tag,
* we need to delay teeing out that byte to signature verifiers.
* Hence, the reading methods of the {@link TeeBCPGInputStream} handle pushing this byte to the output stream using
* {@link DelayedTeeInputStream#squeeze()}.
*/
public class TeeBCPGInputStream {
protected final DelayedTeeInputStream delayedTee;
// InputStream of OpenPGP packets of the current layer
protected final BCPGInputStream packetInputStream;
public TeeBCPGInputStream(BCPGInputStream inputStream, OutputStream outputStream) {
this.delayedTee = new DelayedTeeInputStream(inputStream, outputStream);
this.packetInputStream = BCPGInputStream.wrap(delayedTee);
}
public OpenPgpPacket nextPacketTag() throws IOException {
int tag = packetInputStream.nextPacketTag();
if (tag == -1) {
return null;
}
OpenPgpPacket packet;
try {
packet = OpenPgpPacket.requireFromTag(tag);
} catch (NoSuchElementException e) {
throw e;
}
return packet;
}
public Packet readPacket() throws IOException {
return packetInputStream.readPacket();
}
public PGPCompressedData readCompressedData() throws IOException {
delayedTee.squeeze();
PGPCompressedData compressedData = new PGPCompressedData(packetInputStream);
return compressedData;
}
public PGPLiteralData readLiteralData() throws IOException {
delayedTee.squeeze();
return new PGPLiteralData(packetInputStream);
}
public PGPEncryptedDataList readEncryptedDataList() throws IOException {
delayedTee.squeeze();
return new PGPEncryptedDataList(packetInputStream);
}
public PGPOnePassSignature readOnePassSignature() throws PGPException, IOException {
PGPOnePassSignature onePassSignature = new PGPOnePassSignature(packetInputStream);
delayedTee.squeeze();
return onePassSignature;
}
public PGPSignature readSignature() throws PGPException, IOException {
PGPSignature signature = new PGPSignature(packetInputStream);
delayedTee.squeeze();
return signature;
}
public MarkerPacket readMarker() throws IOException {
MarkerPacket markerPacket = (MarkerPacket) readPacket();
delayedTee.squeeze();
return markerPacket;
}
public void close() throws IOException {
this.packetInputStream.close();
}
public static class DelayedTeeInputStream extends InputStream {
private int last = -1;
private final InputStream inputStream;
private final OutputStream outputStream;
public DelayedTeeInputStream(InputStream inputStream, OutputStream outputStream) {
this.inputStream = inputStream;
this.outputStream = outputStream;
}
@Override
public int read() throws IOException {
if (last != -1) {
outputStream.write(last);
}
try {
last = inputStream.read();
return last;
} catch (IOException e) {
if ("crc check failed in armored message.".equals(e.getMessage())) {
throw e;
}
return -1;
}
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (last != -1) {
outputStream.write(last);
}
int r = inputStream.read(b, off, len);
if (r > 0) {
outputStream.write(b, off, r - 1);
last = b[off + r - 1];
} else {
last = -1;
}
return r;
}
/**
* Squeeze the last byte out and update the output stream.
*
* @throws IOException in case of an IO error
*/
public void squeeze() throws IOException {
if (last != -1) {
outputStream.write(last);
}
last = -1;
}
@Override
public void close() throws IOException {
inputStream.close();
outputStream.close();
}
}
}

View file

@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification.syntax_check;
import org.bouncycastle.openpgp.PGPCompressedData;
import org.bouncycastle.openpgp.PGPEncryptedDataList;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPOnePassSignatureList;
import org.bouncycastle.openpgp.PGPSignatureList;
public enum InputSymbol {
/**
* A {@link PGPLiteralData} packet.
*/
LiteralData,
/**
* A {@link PGPSignatureList} object.
*/
Signature,
/**
* A {@link PGPOnePassSignatureList} object.
*/
OnePassSignature,
/**
* A {@link PGPCompressedData} packet.
* The contents of this packet MUST form a valid OpenPGP message, so a nested PDA is opened to verify
* its nested packet sequence.
*/
CompressedData,
/**
* A {@link PGPEncryptedDataList} object.
* This object combines multiple ESKs and the corresponding Symmetrically Encrypted
* (possibly Integrity Protected) Data packet.
*/
EncryptedData,
/**
* Marks the end of a (sub-) sequence.
* This input is given if the end of an OpenPGP message is reached.
* This might be the case for the end of the whole ciphertext, or the end of a packet with nested contents
* (e.g. the end of a Compressed Data packet).
*/
EndOfSequence
}

View file

@ -0,0 +1,133 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification.syntax_check;
import org.pgpainless.exception.MalformedOpenPgpMessageException;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* This class describes the syntax for OpenPGP messages as specified by rfc4880.
*
* @see <a href="https://www.rfc-editor.org/rfc/rfc4880#section-11.3">
* rfc4880 - §11.3. OpenPGP Messages</a>
* @see <a href="https://blog.jabberhead.tk/2022/09/14/using-pushdown-automata-to-verify-packet-sequences/">
* Blog post about theoretic background and translation of grammar to PDA syntax</a>
* @see <a href="https://blog.jabberhead.tk/2022/10/26/implementing-packet-sequence-validation-using-pushdown-automata/">
* Blog post about practically implementing the PDA for packet syntax validation</a>
*/
public class OpenPgpMessageSyntax implements Syntax {
@Override
public @Nonnull Transition transition(@Nonnull State from, @Nonnull InputSymbol input, @Nullable StackSymbol stackItem)
throws MalformedOpenPgpMessageException {
switch (from) {
case OpenPgpMessage:
return fromOpenPgpMessage(input, stackItem);
case LiteralMessage:
return fromLiteralMessage(input, stackItem);
case CompressedMessage:
return fromCompressedMessage(input, stackItem);
case EncryptedMessage:
return fromEncryptedMessage(input, stackItem);
case Valid:
return fromValid(input, stackItem);
}
throw new MalformedOpenPgpMessageException(from, input, stackItem);
}
Transition fromOpenPgpMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem)
throws MalformedOpenPgpMessageException {
if (stackItem != StackSymbol.msg) {
throw new MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem);
}
switch (input) {
case LiteralData:
return new Transition(State.LiteralMessage);
case Signature:
return new Transition(State.OpenPgpMessage, StackSymbol.msg);
case OnePassSignature:
return new Transition(State.OpenPgpMessage, StackSymbol.ops, StackSymbol.msg);
case CompressedData:
return new Transition(State.CompressedMessage);
case EncryptedData:
return new Transition(State.EncryptedMessage);
case EndOfSequence:
default:
throw new MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem);
}
}
Transition fromLiteralMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem)
throws MalformedOpenPgpMessageException {
switch (input) {
case Signature:
if (stackItem == StackSymbol.ops) {
return new Transition(State.LiteralMessage);
}
break;
case EndOfSequence:
if (stackItem == StackSymbol.terminus) {
return new Transition(State.Valid);
}
break;
}
throw new MalformedOpenPgpMessageException(State.LiteralMessage, input, stackItem);
}
Transition fromCompressedMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem)
throws MalformedOpenPgpMessageException {
switch (input) {
case Signature:
if (stackItem == StackSymbol.ops) {
return new Transition(State.CompressedMessage);
}
break;
case EndOfSequence:
if (stackItem == StackSymbol.terminus) {
return new Transition(State.Valid);
}
break;
}
throw new MalformedOpenPgpMessageException(State.CompressedMessage, input, stackItem);
}
Transition fromEncryptedMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem)
throws MalformedOpenPgpMessageException {
switch (input) {
case Signature:
if (stackItem == StackSymbol.ops) {
return new Transition(State.EncryptedMessage);
}
break;
case EndOfSequence:
if (stackItem == StackSymbol.terminus) {
return new Transition(State.Valid);
}
break;
}
throw new MalformedOpenPgpMessageException(State.EncryptedMessage, input, stackItem);
}
Transition fromValid(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem)
throws MalformedOpenPgpMessageException {
// There is no applicable transition rule out of Valid
throw new MalformedOpenPgpMessageException(State.Valid, input, stackItem);
}
}

View file

@ -0,0 +1,123 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification.syntax_check;
import org.pgpainless.exception.MalformedOpenPgpMessageException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Stack;
import static org.pgpainless.decryption_verification.syntax_check.StackSymbol.msg;
import static org.pgpainless.decryption_verification.syntax_check.StackSymbol.terminus;
public class PDA {
private static final Logger LOGGER = LoggerFactory.getLogger(PDA.class);
// right now we implement what rfc4880 specifies.
// TODO: Consider implementing what we proposed here:
// https://mailarchive.ietf.org/arch/msg/openpgp/uepOF6XpSegMO4c59tt9e5H1i4g/
private final Syntax syntax;
private final Stack<StackSymbol> stack = new Stack<>();
private final List<InputSymbol> inputs = new ArrayList<>(); // Track inputs for debugging / error reporting
private State state;
/**
* Default constructor which initializes the PDA to work with the {@link OpenPgpMessageSyntax}.
*/
public PDA() {
this(new OpenPgpMessageSyntax(), State.OpenPgpMessage, terminus, msg);
}
public PDA(@Nonnull Syntax syntax, @Nonnull State initialState, @Nonnull StackSymbol... initialStack) {
this.syntax = syntax;
this.state = initialState;
for (StackSymbol symbol : initialStack) {
pushStack(symbol);
}
}
public void next(InputSymbol input) throws MalformedOpenPgpMessageException {
StackSymbol stackSymbol = popStack();
try {
Transition transition = syntax.transition(state, input, stackSymbol);
state = transition.getNewState();
for (StackSymbol item : transition.getPushedItems()) {
pushStack(item);
}
inputs.add(input);
} catch (MalformedOpenPgpMessageException e) {
MalformedOpenPgpMessageException wrapped = new MalformedOpenPgpMessageException(
"Malformed message: After reading stream " + Arrays.toString(inputs.toArray()) +
", token '" + input + "' is not allowed." +
"\nNo transition from state '" + state + "' with stack " + Arrays.toString(stack.toArray()) +
(stackSymbol != null ? "||'" + stackSymbol + "'." : "."), e);
LOGGER.debug("Invalid input '" + input + "'", wrapped);
throw wrapped;
}
}
/**
* Return the current state of the PDA.
*
* @return state
*/
public State getState() {
return state;
}
public StackSymbol peekStack() {
if (stack.isEmpty()) {
return null;
}
return stack.peek();
}
/**
* Return true, if the PDA is in a valid state (the OpenPGP message is valid).
*
* @return true if valid, false otherwise
*/
public boolean isValid() {
return getState() == State.Valid && stack.isEmpty();
}
public void assertValid() throws MalformedOpenPgpMessageException {
if (!isValid()) {
throw new MalformedOpenPgpMessageException("Pushdown Automaton is not in an acceptable state: " + toString());
}
}
/**
* Pop an item from the stack.
*
* @return stack item
*/
private StackSymbol popStack() {
if (stack.isEmpty()) {
return null;
}
return stack.pop();
}
/**
* Push an item onto the stack.
*
* @param item item
*/
private void pushStack(StackSymbol item) {
stack.push(item);
}
@Override
public String toString() {
return "State: " + state + " Stack: " + stack;
}
}

View file

@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification.syntax_check;
public enum StackSymbol {
/**
* OpenPGP Message.
*/
msg,
/**
* OnePassSignature (in case of BC this represents a OnePassSignatureList).
*/
ops,
/**
* Special symbol representing the end of the message.
*/
terminus
}

View file

@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification.syntax_check;
/**
* Set of states of the automaton.
*/
public enum State {
OpenPgpMessage,
LiteralMessage,
CompressedMessage,
EncryptedMessage,
Valid
}

View file

@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification.syntax_check;
import org.pgpainless.exception.MalformedOpenPgpMessageException;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* This interface can be used to define a custom syntax for the {@link PDA}.
*/
public interface Syntax {
/**
* Describe a transition rule from {@link State} <pre>from</pre> for {@link InputSymbol} <pre>input</pre>
* with {@link StackSymbol} <pre>stackItem</pre> from the top of the {@link PDA PDAs} stack.
* The resulting {@link Transition} contains the new {@link State}, as well as a list of
* {@link StackSymbol StackSymbols} that get pushed onto the stack by the transition rule.
* If there is no applicable rule, a {@link MalformedOpenPgpMessageException} is thrown, since in this case
* the {@link InputSymbol} must be considered illegal.
*
* @param from current state of the PDA
* @param input input symbol
* @param stackItem item that got popped from the top of the stack
* @return applicable transition rule containing the new state and pushed stack symbols
* @throws MalformedOpenPgpMessageException if there is no applicable transition rule (the input symbol is illegal)
*/
@Nonnull Transition transition(@Nonnull State from, @Nonnull InputSymbol input, @Nullable StackSymbol stackItem)
throws MalformedOpenPgpMessageException;
}

View file

@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification.syntax_check;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Result of applying a transition rule.
* Transition rules can be described by implementing the {@link Syntax} interface.
*/
public class Transition {
private final List<StackSymbol> pushedItems = new ArrayList<>();
private final State newState;
public Transition(@Nonnull State newState, @Nonnull StackSymbol... pushedItems) {
this.newState = newState;
this.pushedItems.addAll(Arrays.asList(pushedItems));
}
/**
* Return the new {@link State} that is reached by applying the transition.
*
* @return new state
*/
@Nonnull
public State getNewState() {
return newState;
}
/**
* Return a list of {@link StackSymbol StackSymbols} that are pushed onto the stack
* by applying the transition.
* The list contains items in the order in which they are pushed onto the stack.
* The list may be empty.
*
* @return list of items to be pushed onto the stack
*/
@Nonnull
public List<StackSymbol> getPushedItems() {
return new ArrayList<>(pushedItems);
}
}

View file

@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Pushdown Automaton to verify validity of packet sequences according to the OpenPGP Message format.
*/
package org.pgpainless.decryption_verification.syntax_check;

View file

@ -1,17 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.exception;
import java.io.IOException;
/**
* Wrapper for {@link IOException} indicating that we need to throw this exception up.
*/
public class FinalIOException extends IOException {
public FinalIOException(IOException e) {
super(e);
}
}

View file

@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.exception;
import org.pgpainless.decryption_verification.syntax_check.InputSymbol;
import org.pgpainless.decryption_verification.syntax_check.StackSymbol;
import org.pgpainless.decryption_verification.syntax_check.State;
/**
* Exception that gets thrown if the OpenPGP message is malformed.
* Malformed messages are messages which do not follow the grammar specified in the RFC.
*
* @see <a href="https://www.rfc-editor.org/rfc/rfc4880#section-11.3">RFC4880 §11.3. OpenPGP Messages</a>
*/
public class MalformedOpenPgpMessageException extends RuntimeException {
public MalformedOpenPgpMessageException(String message) {
super(message);
}
public MalformedOpenPgpMessageException(State state, InputSymbol input, StackSymbol stackItem) {
this("There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack.");
}
public MalformedOpenPgpMessageException(String s, MalformedOpenPgpMessageException e) {
super(s, e);
}
}

View file

@ -1,17 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.exception;
import org.bouncycastle.openpgp.PGPException;
/**
* Exception that gets thrown if a {@link org.bouncycastle.bcpg.LiteralDataPacket} is expected, but not found.
*/
public class MissingLiteralDataException extends PGPException {
public MissingLiteralDataException(String message) {
super(message);
}
}

View file

@ -74,7 +74,7 @@ public class PGPKeyRingCollection {
}
public PGPKeyRingCollection(@Nonnull Collection<PGPKeyRing> collection, boolean isSilent)
throws IOException, PGPException {
throws PGPException {
List<PGPSecretKeyRing> secretKeyRings = new ArrayList<>();
List<PGPPublicKeyRing> publicKeyRings = new ArrayList<>();

View file

@ -1100,10 +1100,17 @@ public class KeyRingInfo {
List<PGPPublicKey> signingKeys = getSigningSubkeys();
for (PGPPublicKey pk : signingKeys) {
PGPSecretKey sk = getSecretKey(pk.getKeyID());
return isSecretKeyAvailable(pk.getKeyID());
}
// No usable secret key found
return false;
}
public boolean isSecretKeyAvailable(long keyId) {
PGPSecretKey sk = getSecretKey(keyId);
if (sk == null) {
// Missing secret key
continue;
return false;
}
S2K s2K = sk.getS2K();
// Unencrypted key
@ -1114,14 +1121,11 @@ public class KeyRingInfo {
// Secret key on smart-card
int s2kType = s2K.getType();
if (s2kType >= 100 && s2kType <= 110) {
continue;
return false;
}
// protected secret key
return true;
}
// No usable secret key found
return false;
}
private KeyAccessor getKeyAccessor(@Nullable String userId, long keyID) {
if (getPublicKey(keyID) == null) {

View file

@ -327,7 +327,7 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface {
@Nonnull SecretKeyRingProtector primaryKeyProtector,
@Nonnull KeyFlag keyFlag,
KeyFlag... additionalKeyFlags)
throws PGPException, IOException, NoSuchAlgorithmException {
throws PGPException, IOException {
KeyFlag[] flags = concat(keyFlag, additionalKeyFlags);
PublicKeyAlgorithm subkeyAlgorithm = PublicKeyAlgorithm.requireFromId(subkey.getPublicKey().getAlgorithm());
SignatureSubpacketsUtil.assureKeyCanCarryFlags(subkeyAlgorithm);

View file

@ -41,7 +41,8 @@ public class KeyRingReader {
* @return key ring
* @throws IOException in case of an IO error
*/
public PGPKeyRing keyRing(@Nonnull InputStream inputStream) throws IOException {
public PGPKeyRing keyRing(@Nonnull InputStream inputStream)
throws IOException {
return readKeyRing(inputStream);
}
@ -53,7 +54,8 @@ public class KeyRingReader {
* @return key ring
* @throws IOException in case of an IO error
*/
public PGPKeyRing keyRing(@Nonnull byte[] bytes) throws IOException {
public PGPKeyRing keyRing(@Nonnull byte[] bytes)
throws IOException {
return keyRing(new ByteArrayInputStream(bytes));
}
@ -65,19 +67,23 @@ public class KeyRingReader {
* @return key ring
* @throws IOException in case of an IO error
*/
public PGPKeyRing keyRing(@Nonnull String asciiArmored) throws IOException {
public PGPKeyRing keyRing(@Nonnull String asciiArmored)
throws IOException {
return keyRing(asciiArmored.getBytes(UTF8));
}
public PGPPublicKeyRing publicKeyRing(@Nonnull InputStream inputStream) throws IOException {
public PGPPublicKeyRing publicKeyRing(@Nonnull InputStream inputStream)
throws IOException {
return readPublicKeyRing(inputStream);
}
public PGPPublicKeyRing publicKeyRing(@Nonnull byte[] bytes) throws IOException {
public PGPPublicKeyRing publicKeyRing(@Nonnull byte[] bytes)
throws IOException {
return publicKeyRing(new ByteArrayInputStream(bytes));
}
public PGPPublicKeyRing publicKeyRing(@Nonnull String asciiArmored) throws IOException {
public PGPPublicKeyRing publicKeyRing(@Nonnull String asciiArmored)
throws IOException {
return publicKeyRing(asciiArmored.getBytes(UTF8));
}
@ -86,23 +92,28 @@ public class KeyRingReader {
return readPublicKeyRingCollection(inputStream);
}
public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull byte[] bytes) throws IOException, PGPException {
public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull byte[] bytes)
throws IOException, PGPException {
return publicKeyRingCollection(new ByteArrayInputStream(bytes));
}
public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull String asciiArmored) throws IOException, PGPException {
public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull String asciiArmored)
throws IOException, PGPException {
return publicKeyRingCollection(asciiArmored.getBytes(UTF8));
}
public PGPSecretKeyRing secretKeyRing(@Nonnull InputStream inputStream) throws IOException {
public PGPSecretKeyRing secretKeyRing(@Nonnull InputStream inputStream)
throws IOException {
return readSecretKeyRing(inputStream);
}
public PGPSecretKeyRing secretKeyRing(@Nonnull byte[] bytes) throws IOException {
public PGPSecretKeyRing secretKeyRing(@Nonnull byte[] bytes)
throws IOException {
return secretKeyRing(new ByteArrayInputStream(bytes));
}
public PGPSecretKeyRing secretKeyRing(@Nonnull String asciiArmored) throws IOException {
public PGPSecretKeyRing secretKeyRing(@Nonnull String asciiArmored)
throws IOException {
return secretKeyRing(asciiArmored.getBytes(UTF8));
}
@ -111,11 +122,13 @@ public class KeyRingReader {
return readSecretKeyRingCollection(inputStream);
}
public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull byte[] bytes) throws IOException, PGPException {
public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull byte[] bytes)
throws IOException, PGPException {
return secretKeyRingCollection(new ByteArrayInputStream(bytes));
}
public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull String asciiArmored) throws IOException, PGPException {
public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull String asciiArmored)
throws IOException, PGPException {
return secretKeyRingCollection(asciiArmored.getBytes(UTF8));
}
@ -124,11 +137,13 @@ public class KeyRingReader {
return readKeyRingCollection(inputStream, isSilent);
}
public PGPKeyRingCollection keyRingCollection(@Nonnull byte[] bytes, boolean isSilent) throws IOException, PGPException {
public PGPKeyRingCollection keyRingCollection(@Nonnull byte[] bytes, boolean isSilent)
throws IOException, PGPException {
return keyRingCollection(new ByteArrayInputStream(bytes), isSilent);
}
public PGPKeyRingCollection keyRingCollection(@Nonnull String asciiArmored, boolean isSilent) throws IOException, PGPException {
public PGPKeyRingCollection keyRingCollection(@Nonnull String asciiArmored, boolean isSilent)
throws IOException, PGPException {
return keyRingCollection(asciiArmored.getBytes(UTF8), isSilent);
}
@ -142,7 +157,8 @@ public class KeyRingReader {
* @return key ring
* @throws IOException in case of an IO error
*/
public static PGPKeyRing readKeyRing(@Nonnull InputStream inputStream) throws IOException {
public static PGPKeyRing readKeyRing(@Nonnull InputStream inputStream)
throws IOException {
return readKeyRing(inputStream, MAX_ITERATIONS);
}
@ -157,7 +173,8 @@ public class KeyRingReader {
* @return key ring
* @throws IOException in case of an IO error
*/
public static PGPKeyRing readKeyRing(@Nonnull InputStream inputStream, int maxIterations) throws IOException {
public static PGPKeyRing readKeyRing(@Nonnull InputStream inputStream, int maxIterations)
throws IOException {
PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(
ArmorUtils.getDecoderStream(inputStream));
int i = 0;
@ -181,7 +198,8 @@ public class KeyRingReader {
throw new IOException("Loop exceeded max iteration count.");
}
public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream) throws IOException {
public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream)
throws IOException {
return readPublicKeyRing(inputStream, MAX_ITERATIONS);
}
@ -196,7 +214,8 @@ public class KeyRingReader {
*
* @throws IOException in case of an IO error or exceeding of max iterations
*/
public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream, int maxIterations) throws IOException {
public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream, int maxIterations)
throws IOException {
PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(
ArmorUtils.getDecoderStream(inputStream));
int i = 0;
@ -218,7 +237,7 @@ public class KeyRingReader {
}
public static PGPPublicKeyRingCollection readPublicKeyRingCollection(@Nonnull InputStream inputStream)
throws IOException, PGPException {
throws IOException {
return readPublicKeyRingCollection(inputStream, MAX_ITERATIONS);
}
@ -232,10 +251,9 @@ public class KeyRingReader {
* @return public key ring collection
*
* @throws IOException in case of an IO error or exceeding of max iterations
* @throws PGPException in case of a broken key
*/
public static PGPPublicKeyRingCollection readPublicKeyRingCollection(@Nonnull InputStream inputStream, int maxIterations)
throws IOException, PGPException {
throws IOException {
PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(
ArmorUtils.getDecoderStream(inputStream));
@ -265,7 +283,8 @@ public class KeyRingReader {
throw new IOException("Loop exceeded max iteration count.");
}
public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream) throws IOException {
public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream)
throws IOException {
return readSecretKeyRing(inputStream, MAX_ITERATIONS);
}
@ -280,7 +299,8 @@ public class KeyRingReader {
*
* @throws IOException in case of an IO error or exceeding of max iterations
*/
public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream, int maxIterations) throws IOException {
public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream, int maxIterations)
throws IOException {
InputStream decoderStream = ArmorUtils.getDecoderStream(inputStream);
PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decoderStream);
int i = 0;
@ -303,7 +323,7 @@ public class KeyRingReader {
}
public static PGPSecretKeyRingCollection readSecretKeyRingCollection(@Nonnull InputStream inputStream)
throws IOException, PGPException {
throws IOException {
return readSecretKeyRingCollection(inputStream, MAX_ITERATIONS);
}
@ -317,11 +337,10 @@ public class KeyRingReader {
* @return secret key ring collection
*
* @throws IOException in case of an IO error or exceeding of max iterations
* @throws PGPException in case of a broken secret key
*/
public static PGPSecretKeyRingCollection readSecretKeyRingCollection(@Nonnull InputStream inputStream,
int maxIterations)
throws IOException, PGPException {
throws IOException {
PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(
ArmorUtils.getDecoderStream(inputStream));

View file

@ -30,4 +30,8 @@ public final class KeyIdUtil {
return new BigInteger(longKeyId, 16).longValue();
}
public static String formatKeyId(long keyId) {
return Long.toHexString(keyId).toUpperCase();
}
}

View file

@ -144,6 +144,17 @@ public final class KeyRingUtils {
return secretKey;
}
@Nonnull
public static PGPPublicKeyRing publicKeys(@Nonnull PGPKeyRing keys) {
if (keys instanceof PGPPublicKeyRing) {
return (PGPPublicKeyRing) keys;
} else if (keys instanceof PGPSecretKeyRing) {
return publicKeyRingFrom((PGPSecretKeyRing) keys);
} else {
throw new IllegalArgumentException("Unknown keys class: " + keys.getClass().getName());
}
}
/**
* Extract a {@link PGPPublicKeyRing} containing all public keys from the provided {@link PGPSecretKeyRing}.
*
@ -167,12 +178,9 @@ public final class KeyRingUtils {
*
* @param secretKeyRings secret key ring collection
* @return public key ring collection
* @throws PGPException TODO: remove
* @throws IOException TODO: remove
*/
@Nonnull
public static PGPPublicKeyRingCollection publicKeyRingCollectionFrom(@Nonnull PGPSecretKeyRingCollection secretKeyRings)
throws PGPException, IOException {
public static PGPPublicKeyRingCollection publicKeyRingCollectionFrom(@Nonnull PGPSecretKeyRingCollection secretKeyRings) {
List<PGPPublicKeyRing> certificates = new ArrayList<>();
for (PGPSecretKeyRing secretKey : secretKeyRings) {
certificates.add(PGPainless.extractCertificate(secretKey));
@ -200,13 +208,9 @@ public final class KeyRingUtils {
*
* @param rings array of public key rings
* @return key ring collection
*
* @throws IOException in case of an io error
* @throws PGPException in case of a broken key
*/
@Nonnull
public static PGPPublicKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPPublicKeyRing... rings)
throws IOException, PGPException {
public static PGPPublicKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPPublicKeyRing... rings) {
return new PGPPublicKeyRingCollection(Arrays.asList(rings));
}
@ -215,13 +219,9 @@ public final class KeyRingUtils {
*
* @param rings array of secret key rings
* @return secret key ring collection
*
* @throws IOException in case of an io error
* @throws PGPException in case of a broken key
*/
@Nonnull
public static PGPSecretKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPSecretKeyRing... rings)
throws IOException, PGPException {
public static PGPSecretKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPSecretKeyRing... rings) {
return new PGPSecretKeyRingCollection(Arrays.asList(rings));
}

View file

@ -237,7 +237,7 @@ public class PublicKeyParameterValidationUtil {
* Validate ElGamal public key parameters.
*
* Original implementation by the openpgpjs authors:
* https://github.com/openpgpjs/openpgpjs/blob/main/src/crypto/public_key/elgamal.js#L76-L143
* <a href="https://github.com/openpgpjs/openpgpjs/blob/main/src/crypto/public_key/elgamal.js#L76-L143>OpenPGP.js source</a>
* @param secretKey secret key
* @param publicKey public key
* @return true if supposedly valid, false if invalid

View file

@ -13,19 +13,19 @@ import org.pgpainless.key.SubkeyIdentifier;
* Tuple-class which bundles together a signature, the signing key that created the signature,
* an identifier of the signing key and a record of whether the signature was verified.
*/
public class DetachedSignatureCheck {
public class SignatureCheck {
private final PGPSignature signature;
private final PGPKeyRing signingKeyRing;
private final SubkeyIdentifier signingKeyIdentifier;
/**
* Create a new {@link DetachedSignatureCheck} object.
* Create a new {@link SignatureCheck} object.
*
* @param signature signature
* @param signingKeyRing signing key that created the signature
* @param signingKeyIdentifier identifier of the used signing key
*/
public DetachedSignatureCheck(PGPSignature signature, PGPKeyRing signingKeyRing, SubkeyIdentifier signingKeyIdentifier) {
public SignatureCheck(PGPSignature signature, PGPKeyRing signingKeyRing, SubkeyIdentifier signingKeyIdentifier) {
this.signature = signature;
this.signingKeyRing = signingKeyRing;
this.signingKeyIdentifier = signingKeyIdentifier;

View file

@ -178,6 +178,7 @@ public final class ArmorUtils {
* If it is <pre>false</pre>, the signature will be encoded as-is.
*
* @param signature signature
* @param export whether to exclude non-exportable subpackets or trust-packets.
* @return ascii armored string
*
* @throws IOException in case of an error in the {@link ArmoredOutputStream}

View file

@ -1,54 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.util;
public final class BCUtil {
private BCUtil() {
}
/**
* A constant time equals comparison - does not terminate early if
* test will fail. For best results always pass the expected value
* as the first parameter.
*
* TODO: This method was proposed as a patch to BC:
* https://github.com/bcgit/bc-java/pull/1141
* Replace usage of this method with upstream eventually.
* Remove once BC 172 gets released, given it contains the patch.
*
* @param expected first array
* @param supplied second array
* @return true if arrays equal, false otherwise.
*/
public static boolean constantTimeAreEqual(
char[] expected,
char[] supplied) {
if (expected == null || supplied == null) {
return false;
}
if (expected == supplied) {
return true;
}
int len = Math.min(expected.length, supplied.length);
int nonEqual = expected.length ^ supplied.length;
// do the char-wise comparison
for (int i = 0; i != len; i++) {
nonEqual |= (expected[i] ^ supplied[i]);
}
// If supplied is longer than expected, iterate over rest of supplied with NOPs
for (int i = len; i < supplied.length; i++) {
nonEqual |= ((byte) supplied[i] ^ (byte) ~supplied[i]);
}
return nonEqual == 0;
}
}

View file

@ -8,8 +8,6 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Arrays;
import static org.pgpainless.util.BCUtil.constantTimeAreEqual;
public class Passphrase {
public final Object lock = new Object();
@ -165,6 +163,6 @@ public class Passphrase {
}
Passphrase other = (Passphrase) obj;
return (getChars() == null && other.getChars() == null) ||
constantTimeAreEqual(getChars(), other.getChars());
org.bouncycastle.util.Arrays.constantTimeAreEqual(getChars(), other.getChars());
}
}

View file

@ -4,8 +4,7 @@
package investigations;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@ -32,9 +31,8 @@ import org.pgpainless.algorithm.SignatureType;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.decryption_verification.ConsumerOptions;
import org.pgpainless.decryption_verification.DecryptionStream;
import org.pgpainless.decryption_verification.OpenPgpMetadata;
import org.pgpainless.exception.MalformedOpenPgpMessageException;
import org.pgpainless.implementation.ImplementationFactory;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.key.info.KeyRingInfo;
import org.pgpainless.key.protection.UnlockSecretKey;
import org.pgpainless.util.Passphrase;
@ -177,7 +175,7 @@ public class InvestigateMultiSEIPMessageHandlingTest {
}
@Test
public void testDecryptAndVerifyDoesIgnoreAppendedSEIPData() throws IOException, PGPException {
public void testDecryptAndVerifyDetectsAppendedSEIPData() throws IOException, PGPException {
PGPSecretKeyRing ring1 = PGPainless.readKeyRing().secretKeyRing(KEY1);
PGPSecretKeyRing ring2 = PGPainless.readKeyRing().secretKeyRing(KEY2);
@ -191,15 +189,6 @@ public class InvestigateMultiSEIPMessageHandlingTest {
.withOptions(options);
ByteArrayOutputStream out = new ByteArrayOutputStream();
Streams.pipeAll(decryptionStream, out);
decryptionStream.close();
assertArrayEquals(data1.getBytes(StandardCharsets.UTF_8), out.toByteArray());
OpenPgpMetadata metadata = decryptionStream.getResult();
assertEquals(1, metadata.getVerifiedSignatures().size(),
"The first SEIP packet is signed exactly only by the signing key of ring1.");
assertEquals(
new SubkeyIdentifier(ring1, new KeyRingInfo(ring1).getSigningSubkeys().get(0).getKeyID()),
metadata.getVerifiedSignatures().keySet().iterator().next());
assertThrows(MalformedOpenPgpMessageException.class, () -> Streams.pipeAll(decryptionStream, out));
}
}

View file

@ -489,7 +489,7 @@ public class AsciiArmorCRCTests {
Passphrase passphrase = Passphrase.fromPassword("flowcrypt compatibility tests");
@Test
public void testInvalidArmorCRCThrowsOnClose() throws PGPException, IOException {
public void testInvalidArmorCRCThrowsOnClose() throws IOException {
String message = "-----BEGIN PGP MESSAGE-----\n" +
"Version: FlowCrypt 5.0.4 Gmail Encryption flowcrypt.com\n" +
"Comment: Seamlessly send, receive and search encrypted email\n" +
@ -542,6 +542,7 @@ public class AsciiArmorCRCTests {
"-----END PGP MESSAGE-----\n";
PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(ASCII_KEY);
assertThrows(IOException.class, () -> {
DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify()
.onInputStream(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)))
.withOptions(new ConsumerOptions().addDecryptionKey(
@ -550,6 +551,7 @@ public class AsciiArmorCRCTests {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Streams.pipeAll(decryptionStream, outputStream);
assertThrows(IOException.class, decryptionStream::close);
decryptionStream.close();
});
}
}

View file

@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.bouncycastle;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.util.io.Streams;
import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.EncryptionPurpose;
import org.pgpainless.decryption_verification.ConsumerOptions;
import org.pgpainless.decryption_verification.DecryptionStream;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.key.info.KeyRingInfo;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.key.protection.UnlockSecretKey;
public class CachingBcPublicKeyDataDecryptorFactoryTest {
private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
"Version: PGPainless\n" +
"Comment: C8AE 4279 5958 5F46 86A9 8B5F EC69 7C29 2BE4 44E0\n" +
"Comment: Alice\n" +
"\n" +
"lFgEY1vEcxYJKwYBBAHaRw8BAQdAXOUK1uc1iBeM+mMt2nLCukXWoJd/SodrtN9S\n" +
"U/zzwu0AAP9eePPw91KLuq6PF9jQoTRz/cW4CyiALNJpsOJIZ1rp3xOBtAVBbGlj\n" +
"ZYiPBBMWCgBBBQJjW8RzCRDsaXwpK+RE4BYhBMiuQnlZWF9GhqmLX+xpfCkr5ETg\n" +
"Ap4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAGqWAQC8oz7l8izjUis5ji+sgI+q\n" +
"gML22VNybqmLBpzZwnNU5wEApe9fNTRbK5yAITGBscxH7o74Qe+CLI6Ni5MwzKxr\n" +
"5AucXQRjW8RzEgorBgEEAZdVAQUBAQdAm8xk0QSvpp2ZU1KQ31E7eEZYLKpbW4JE\n" +
"opmtMQx6AlIDAQgHAAD/XTb/qSosfkNvli3BQiUzVRAqKaU4PKAq7at6afxoYSgN\n" +
"4Yh1BBgWCgAdBQJjW8RzAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQ7Gl8KSvk\n" +
"ROB38QEA0MvDt0bjEXwFoM0E34z0MtPcG3VBYcQ+iFRIqFfEl5UA/2yZxFjoZqrs\n" +
"AQE8TaVpXYfbc2p/GEKA9LGd9l/g0QQLnFgEY1vEcxYJKwYBBAHaRw8BAQdAyCOv\n" +
"6hGUvHcCBSDKP3fRz+scyJ9zwMt7nFXK5A/k2YgAAQCn3Es+IhvePn3eBlcYMMr0\n" +
"xcktrY1NJAIZPfjlUJ0J1g6LiNUEGBYKAH0FAmNbxHMCngECmwIFFgIDAQAECwkI\n" +
"BwUVCgkIC18gBBkWCgAGBQJjW8RzAAoJECxLf7KoUc8wD18BANNpIr4E+RRVVztR\n" +
"OVwdxSe0SRWGjkW8nHrRyghHKTuMAP9p4ZKicOYA1uZbiNNjyuJuS8xBH6Hihurb\n" +
"gDypVgxdBQAKCRDsaXwpK+RE4EQjAP9ARZEPxKNLFkrvjoZ8nrts3qhv3VtMrU+9\n" +
"huZnYLe1FQEAtgO6V7wutHvVARHXqPJ6lcv+SueIu+BjLFYEKuBwggs=\n" +
"=ShJd\n" +
"-----END PGP PRIVATE KEY BLOCK-----";
private static final String MSG = "-----BEGIN PGP MESSAGE-----\n" +
"Version: PGPainless\n" +
"\n" +
"hF4DJmQMTBqw3G8SAQdALkHpO0UkS/CqkwxUz74MJU3PV72ZrIL8ZcrO8ofhblkw\n" +
"iDIhSwwGTG3tj+sG+ZVWKsmONKi7Om5seJDHQtQ8MfdCELAgwYHSt6MrgDBhuDIH\n" +
"0kABZhq2/8qk3EGXPpc+xxs4r4g8SgHOiiHSim5NGtounXXIaF6T/hUmlorkeYf/\n" +
"a9pCC0QXRUAr8NOcdsfbvb5V\n" +
"=dQa8\n" +
"-----END PGP MESSAGE-----";
@Test
public void test() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException {
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY);
SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys();
KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys);
SubkeyIdentifier decryptionKey = new SubkeyIdentifier(secretKeys,
info.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0).getKeyID());
PGPSecretKey secretKey = secretKeys.getSecretKey(decryptionKey.getSubkeyId());
PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector);
CachingBcPublicKeyDataDecryptorFactory cachingFactory = new CachingBcPublicKeyDataDecryptorFactory(
privateKey, decryptionKey);
ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(MSG.getBytes());
DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify()
.onInputStream(ciphertextIn)
.withOptions(ConsumerOptions.get()
.addCustomDecryptorFactory(cachingFactory));
ByteArrayOutputStream out = new ByteArrayOutputStream();
Streams.pipeAll(decryptionStream, out);
decryptionStream.close();
decryptionStream = PGPainless.decryptAndOrVerify()
.onInputStream(ciphertextIn)
.withOptions(ConsumerOptions.get()
.addCustomDecryptorFactory(cachingFactory));
out = new ByteArrayOutputStream();
Streams.pipeAll(decryptionStream, out);
decryptionStream.close();
}
}

View file

@ -28,7 +28,7 @@ public class PGPPublicKeyRingTest {
* Learning test to see if BC also makes userids available on subkeys.
* It does not.
*
* see also https://security.stackexchange.com/questions/92635/is-it-possible-to-assign-different-uids-to-subkeys-for-the-purpose-of-having-mul
* @see <a href="https://security.stackexchange.com/questions/92635/is-it-possible-to-assign-different-uids-to-subkeys-for-the-purpose-of-having-mul>Stackexchange link</a>
*/
@Test
public void subkeysDoNotHaveUserIDsTest() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException {

View file

@ -0,0 +1,277 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.gnupg;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.bouncycastle.bcpg.S2K;
import org.bouncycastle.bcpg.SecretKeyPacket;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.key.util.KeyIdUtil;
public class GnuPGDummyKeyUtilTest {
// normal, non-hw-backed key
private static final String FULL_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
"Version: PGPainless\n" +
"Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" +
"Comment: Hardy Hardware <hardy@hard.ware>\n" +
"\n" +
"lFgEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" +
"eBwCeTQAAPwJN+Xmr0jjN7RA9jgqXnxC/rcWHmdp/j9NdEd7K2Wbxw/rtCBIYXJk\n" +
"eSBIYXJkd2FyZSA8aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9\n" +
"qfdqTxYhBAH9q2zgSlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJ\n" +
"CAsCmQEAAPk2AP922T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1M\n" +
"l9qqx6QGcaNKe8deMe3EhTant6mS9tqMHp2/3gmcXQRjW9KIEgorBgEEAZdVAQUB\n" +
"AQdAVXBLNvNmFh9KX6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgHAAD/fw9hnzeH\n" +
"VtBaHi6efXvnc4rdVj8zWk0LKo1clFd3bTAN+oh1BBgWCgAdBQJjW9KIAp4BApsM\n" +
"BRYCAwEABAsJCAcFFQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdi\n" +
"Ags0yZrQPkMs6eL+83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUT\n" +
"vP0BnFgEY1vSiBYJKwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D\n" +
"6nfwJtbDT0YAAQCgnCsN9iX7s2TQd8NPggWs4QdhaFpb6olt3SlAvUy/wRBDiNUE\n" +
"GBYKAH0FAmNb0ogCngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KI\n" +
"AAoJEJQCL6VtwFtJDmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5f\n" +
"AQD2oLSEW1GOgIs64+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6\n" +
"AP9Xftw8xZ7/MWhYImk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwG\n" +
"jF6AYiLOzO/R1x5bSlYD3FeJ3Qo=\n" +
"=+vXp\n" +
"-----END PGP PRIVATE KEY BLOCK-----\n";
private static final long primaryKeyId = KeyIdUtil.fromLongKeyId("C312C97DA9F76A4F");
private static final long encryptionKeyId = KeyIdUtil.fromLongKeyId("6924D066714CE8C6");
private static final long signatureKeyId = KeyIdUtil.fromLongKeyId("94022FA56DC05B49");
private static final byte[] cardSerial = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f};
public static final String ALL_KEYS_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
"Version: PGPainless\n" +
"Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" +
"Comment: Hardy Hardware <hardy@hard.ware>\n" +
"\n" +
"lEwEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" +
"eBwCeTT+AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PtCBIYXJkeSBIYXJkd2FyZSA8\n" +
"aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9qfdqTxYhBAH9q2zg\n" +
"SlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAPk2AP92\n" +
"2T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1Ml9qqx6QGcaNKe8de\n" +
"Me3EhTant6mS9tqMHp2/3gmcUQRjW9KIEgorBgEEAZdVAQUBAQdAVXBLNvNmFh9K\n" +
"X6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgH/gBlAEdOVQIQAAECAwQFBgcICQoL\n" +
"DA0OD4h1BBgWCgAdBQJjW9KIAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQwxLJ\n" +
"fan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdiAgs0yZrQPkMs6eL+83te770A/jG0DeJy\n" +
"+88fOfWTj+mixO98PZPnQ0MybWC/1QUTvP0BnEwEY1vSiBYJKwYBBAHaRw8BAQdA\n" +
"vSYTD60t8vx10dSEBACUoIfVCpeOB30D6nfwJtbDT0b+AGUAR05VAhAAAQIDBAUG\n" +
"BwgJCgsMDQ4PiNUEGBYKAH0FAmNb0ogCngECmwIFFgIDAQAECwkIBwUVCgkIC18g\n" +
"BBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJDmMBAKqsGfRFQxJXyPgugWBgEaO5lt9f\n" +
"MM0yUxa76cmSWe5fAQD2oLSEW1GOgIs64+Z3gvtXopmeupT09HhI7ger98zDAwAK\n" +
"CRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhYImk/xheqPy07K4qo3T1pGKUvUqjWQQEA\n" +
"hE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5bSlYD3FeJ3Qo=\n" +
"=rYoa\n" +
"-----END PGP PRIVATE KEY BLOCK-----";
public static final String ALL_KEYS_REMOVED = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
"Version: PGPainless\n" +
"Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" +
"Comment: Hardy Hardware <hardy@hard.ware>\n" +
"\n" +
"lDsEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" +
"eBwCeTT+AGUAR05VAbQgSGFyZHkgSGFyZHdhcmUgPGhhcmR5QGhhcmQud2FyZT6I\n" +
"jwQTFgoAQQUCY1vSiAkQwxLJfan3ak8WIQQB/ats4EpQeHn+ShjDEsl9qfdqTwKe\n" +
"AQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAAD5NgD/dtk+U0O4bpBZacV904TIYniZ\n" +
"xAhmORKreVNP7xGNV3YA/3hNTJfaqsekBnGjSnvHXjHtxIU2p7epkvbajB6dv94J\n" +
"nEAEY1vSiBIKKwYBBAGXVQEFAQEHQFVwSzbzZhYfSl+oi5nTSTNvGXPTxp8xKAA/\n" +
"fk+KdJQ8AwEIB/4AZQBHTlUBiHUEGBYKAB0FAmNb0ogCngECmwwFFgIDAQAECwkI\n" +
"BwUVCgkICwAKCRDDEsl9qfdqT8nJAP0YGPS+O1hkB/kWLR4Qp2ICCzTJmtA+Qyzp\n" +
"4v7ze17vvQD+MbQN4nL7zx859ZOP6aLE73w9k+dDQzJtYL/VBRO8/QGcOwRjW9KI\n" +
"FgkrBgEEAdpHDwEBB0C9JhMPrS3y/HXR1IQEAJSgh9UKl44HfQPqd/Am1sNPRv4A\n" +
"ZQBHTlUBiNUEGBYKAH0FAmNb0ogCngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkW\n" +
"CgAGBQJjW9KIAAoJEJQCL6VtwFtJDmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0y\n" +
"Uxa76cmSWe5fAQD2oLSEW1GOgIs64+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDD\n" +
"Esl9qfdqTwR6AP9Xftw8xZ7/MWhYImk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r\n" +
"0oTcJn+KVCwGjF6AYiLOzO/R1x5bSlYD3FeJ3Qo=\n" +
"=GEN/\n" +
"-----END PGP PRIVATE KEY BLOCK-----";
public static final String PRIMARY_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
"Version: PGPainless\n" +
"Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" +
"Comment: Hardy Hardware <hardy@hard.ware>\n" +
"\n" +
"lEwEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" +
"eBwCeTT+AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PtCBIYXJkeSBIYXJkd2FyZSA8\n" +
"aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9qfdqTxYhBAH9q2zg\n" +
"SlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAPk2AP92\n" +
"2T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1Ml9qqx6QGcaNKe8de\n" +
"Me3EhTant6mS9tqMHp2/3gmcXQRjW9KIEgorBgEEAZdVAQUBAQdAVXBLNvNmFh9K\n" +
"X6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgHAAD/fw9hnzeHVtBaHi6efXvnc4rd\n" +
"Vj8zWk0LKo1clFd3bTAN+oh1BBgWCgAdBQJjW9KIAp4BApsMBRYCAwEABAsJCAcF\n" +
"FQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdiAgs0yZrQPkMs6eL+\n" +
"83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUTvP0BnFgEY1vSiBYJ\n" +
"KwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D6nfwJtbDT0YAAQCg\n" +
"nCsN9iX7s2TQd8NPggWs4QdhaFpb6olt3SlAvUy/wRBDiNUEGBYKAH0FAmNb0ogC\n" +
"ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJ\n" +
"DmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5fAQD2oLSEW1GOgIs6\n" +
"4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" +
"Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" +
"SlYD3FeJ3Qo=\n" +
"=zQLi\n" +
"-----END PGP PRIVATE KEY BLOCK-----";
public static final String ENCRYPTION_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
"Version: PGPainless\n" +
"Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" +
"Comment: Hardy Hardware <hardy@hard.ware>\n" +
"\n" +
"lFgEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" +
"eBwCeTQAAPwJN+Xmr0jjN7RA9jgqXnxC/rcWHmdp/j9NdEd7K2Wbxw/rtCBIYXJk\n" +
"eSBIYXJkd2FyZSA8aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9\n" +
"qfdqTxYhBAH9q2zgSlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJ\n" +
"CAsCmQEAAPk2AP922T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1M\n" +
"l9qqx6QGcaNKe8deMe3EhTant6mS9tqMHp2/3gmcUQRjW9KIEgorBgEEAZdVAQUB\n" +
"AQdAVXBLNvNmFh9KX6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgH/gBlAEdOVQIQ\n" +
"AAECAwQFBgcICQoLDA0OD4h1BBgWCgAdBQJjW9KIAp4BApsMBRYCAwEABAsJCAcF\n" +
"FQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdiAgs0yZrQPkMs6eL+\n" +
"83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUTvP0BnFgEY1vSiBYJ\n" +
"KwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D6nfwJtbDT0YAAQCg\n" +
"nCsN9iX7s2TQd8NPggWs4QdhaFpb6olt3SlAvUy/wRBDiNUEGBYKAH0FAmNb0ogC\n" +
"ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJ\n" +
"DmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5fAQD2oLSEW1GOgIs6\n" +
"4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" +
"Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" +
"SlYD3FeJ3Qo=\n" +
"=7OZu\n" +
"-----END PGP PRIVATE KEY BLOCK-----";
public static final String SIGNATURE_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
"Version: PGPainless\n" +
"Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" +
"Comment: Hardy Hardware <hardy@hard.ware>\n" +
"\n" +
"lFgEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" +
"eBwCeTQAAPwJN+Xmr0jjN7RA9jgqXnxC/rcWHmdp/j9NdEd7K2Wbxw/rtCBIYXJk\n" +
"eSBIYXJkd2FyZSA8aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9\n" +
"qfdqTxYhBAH9q2zgSlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJ\n" +
"CAsCmQEAAPk2AP922T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1M\n" +
"l9qqx6QGcaNKe8deMe3EhTant6mS9tqMHp2/3gmcXQRjW9KIEgorBgEEAZdVAQUB\n" +
"AQdAVXBLNvNmFh9KX6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgHAAD/fw9hnzeH\n" +
"VtBaHi6efXvnc4rdVj8zWk0LKo1clFd3bTAN+oh1BBgWCgAdBQJjW9KIAp4BApsM\n" +
"BRYCAwEABAsJCAcFFQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdi\n" +
"Ags0yZrQPkMs6eL+83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUT\n" +
"vP0BnEwEY1vSiBYJKwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D\n" +
"6nfwJtbDT0b+AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PiNUEGBYKAH0FAmNb0ogC\n" +
"ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJ\n" +
"DmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5fAQD2oLSEW1GOgIs6\n" +
"4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" +
"Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" +
"SlYD3FeJ3Qo=\n" +
"=GpEw\n" +
"-----END PGP PRIVATE KEY BLOCK-----";
@Test
public void testMoveAllKeysToCard() throws IOException {
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY);
PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ALL_KEYS_ON_CARD);
PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys)
.divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.any(), cardSerial);
for (PGPSecretKey key : onCard) {
assertEquals(SecretKeyPacket.USAGE_SHA1, key.getS2KUsage());
S2K s2K = key.getS2K();
assertEquals(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD, s2K.getProtectionMode());
}
assertArrayEquals(expected.getEncoded(), onCard.getEncoded());
}
@Test
public void testMovePrimaryKeyToCard() throws IOException {
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY);
PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(PRIMARY_KEY_ON_CARD);
PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys)
.divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(primaryKeyId), cardSerial);
assertArrayEquals(expected.getEncoded(), onCard.getEncoded());
}
@Test
public void testMoveEncryptionKeyToCard() throws IOException {
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY);
PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ENCRYPTION_KEY_ON_CARD);
PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys)
.divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(encryptionKeyId), cardSerial);
assertArrayEquals(expected.getEncoded(), onCard.getEncoded());
}
@Test
public void testMoveSigningKeyToCard() throws IOException {
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY);
PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(SIGNATURE_KEY_ON_CARD);
PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys)
.divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(signatureKeyId), cardSerial);
assertArrayEquals(expected.getEncoded(), onCard.getEncoded());
}
@Test
public void testRemoveAllKeys() throws IOException {
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY);
PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ALL_KEYS_REMOVED);
PGPSecretKeyRing removedSecretKeys = GnuPGDummyKeyUtil.modify(secretKeys)
.removePrivateKeys(GnuPGDummyKeyUtil.KeyFilter.any());
for (PGPSecretKey key : removedSecretKeys) {
assertEquals(key.getS2KUsage(), SecretKeyPacket.USAGE_SHA1);
S2K s2k = key.getS2K();
assertEquals(GnuPGDummyExtension.NO_PRIVATE_KEY.getId(), s2k.getProtectionMode());
}
assertArrayEquals(expected.getEncoded(), removedSecretKeys.getEncoded());
}
@Test
public void testGetSingleIdOfHardwareBackedKey() throws IOException {
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY);
assertTrue(GnuPGDummyKeyUtil.getIdsOfKeysWithGnuPGS2KDivertedToCard(secretKeys).isEmpty());
PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys)
.divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(encryptionKeyId));
Set<SubkeyIdentifier> hardwareBackedKeys = GnuPGDummyKeyUtil
.getIdsOfKeysWithGnuPGS2KDivertedToCard(withHardwareBackedEncryptionKey);
assertEquals(Collections.singleton(new SubkeyIdentifier(secretKeys, encryptionKeyId)), hardwareBackedKeys);
}
@Test
public void testGetIdsOfFullyHardwareBackedKey() throws IOException {
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY);
assertTrue(GnuPGDummyKeyUtil.getIdsOfKeysWithGnuPGS2KDivertedToCard(secretKeys).isEmpty());
PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys)
.divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.any());
Set<SubkeyIdentifier> expected = new HashSet<>();
for (PGPSecretKey key : secretKeys) {
expected.add(new SubkeyIdentifier(secretKeys, key.getKeyID()));
}
Set<SubkeyIdentifier> hardwareBackedKeys = GnuPGDummyKeyUtil
.getIdsOfKeysWithGnuPGS2KDivertedToCard(withHardwareBackedEncryptionKey);
assertEquals(expected, hardwareBackedKeys);
}
}

View file

@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
import org.bouncycastle.bcpg.PacketTags;
import org.junit.jupiter.api.Test;
import java.util.NoSuchElementException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class OpenPgpPacketTest {
@Test
public void testFromInvalidTag() {
int tag = PacketTags.RESERVED;
assertNull(OpenPgpPacket.fromTag(tag));
assertThrows(NoSuchElementException.class,
() -> OpenPgpPacket.requireFromTag(tag));
}
@Test
public void testFromExistingTags() {
for (OpenPgpPacket p : OpenPgpPacket.values()) {
assertEquals(p, OpenPgpPacket.fromTag(p.getTag()));
assertEquals(p, OpenPgpPacket.requireFromTag(p.getTag()));
}
}
@Test
public void testPKESKTagMatches() {
assertEquals(PacketTags.PUBLIC_KEY_ENC_SESSION, OpenPgpPacket.PKESK.getTag());
}
}

View file

@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory;
import org.bouncycastle.util.io.Streams;
import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.EncryptionPurpose;
import org.pgpainless.encryption_signing.EncryptionOptions;
import org.pgpainless.encryption_signing.EncryptionStream;
import org.pgpainless.encryption_signing.ProducerOptions;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.key.info.KeyRingInfo;
import org.pgpainless.key.protection.UnlockSecretKey;
import org.pgpainless.util.Passphrase;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class CustomPublicKeyDataDecryptorFactoryTest {
@Test
public void testDecryptionWithEmulatedHardwareDecryptionCallback()
throws PGPException, IOException, InvalidAlgorithmParameterException, NoSuchAlgorithmException {
PGPSecretKeyRing secretKey = PGPainless.generateKeyRing().modernKeyRing("Alice");
PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKey);
KeyRingInfo info = PGPainless.inspectKeyRing(secretKey);
PGPPublicKey encryptionKey = info.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0);
// Encrypt a test message
String plaintext = "Hello, World!\n";
ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream();
EncryptionStream encryptionStream = PGPainless.encryptAndOrSign()
.onOutputStream(ciphertextOut)
.withOptions(ProducerOptions.encrypt(EncryptionOptions.get()
.addRecipient(cert)));
encryptionStream.write(plaintext.getBytes(StandardCharsets.UTF_8));
encryptionStream.close();
HardwareSecurity.DecryptionCallback hardwareDecryptionCallback = new HardwareSecurity.DecryptionCallback() {
@Override
public byte[] decryptSessionKey(long keyId, int keyAlgorithm, byte[] sessionKeyData)
throws HardwareSecurity.HardwareSecurityException {
// Emulate hardware decryption.
try {
PGPSecretKey decryptionKey = secretKey.getSecretKey(encryptionKey.getKeyID());
PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, Passphrase.emptyPassphrase());
PublicKeyDataDecryptorFactory internal = new BcPublicKeyDataDecryptorFactory(privateKey);
return internal.recoverSessionData(keyAlgorithm, new byte[][] {sessionKeyData});
} catch (PGPException e) {
throw new HardwareSecurity.HardwareSecurityException();
}
}
};
// Decrypt
DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify()
.onInputStream(new ByteArrayInputStream(ciphertextOut.toByteArray()))
.withOptions(ConsumerOptions.get()
.addCustomDecryptorFactory(
new HardwareSecurity.HardwareDataDecryptorFactory(
new SubkeyIdentifier(cert, encryptionKey.getKeyID()),
hardwareDecryptionCallback)));
ByteArrayOutputStream decryptedOut = new ByteArrayOutputStream();
Streams.pipeAll(decryptionStream, decryptedOut);
decryptionStream.close();
assertEquals(plaintext, decryptedOut.toString());
}
}

View file

@ -0,0 +1,84 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import org.junit.JUtils;
import org.junit.jupiter.api.Test;
import org.pgpainless.algorithm.CompressionAlgorithm;
import org.pgpainless.algorithm.StreamEncoding;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.util.DateUtil;
import java.util.Date;
import java.util.Iterator;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class MessageMetadataTest {
@Test
public void processTestMessage_COMP_ENC_ENC_LIT() {
// Note: COMP of ENC does not make sense, since ENC is indistinguishable from randomness
// and randomness cannot be encrypted.
// For the sake of testing though, this is okay.
MessageMetadata.Message message = new MessageMetadata.Message();
MessageMetadata.CompressedData compressedData = new MessageMetadata.CompressedData(CompressionAlgorithm.ZIP, message.depth + 1);
MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_128, compressedData.depth + 1);
MessageMetadata.EncryptedData encryptedData1 = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_256, encryptedData.depth + 1);
MessageMetadata.LiteralData literalData = new MessageMetadata.LiteralData();
message.setChild(compressedData);
compressedData.setChild(encryptedData);
encryptedData.setChild(encryptedData1);
encryptedData1.setChild(literalData);
MessageMetadata metadata = new MessageMetadata(message);
// Check encryption algs
assertEquals(SymmetricKeyAlgorithm.AES_128, metadata.getEncryptionAlgorithm(), "getEncryptionAlgorithm() returns alg of outermost EncryptedData");
Iterator<SymmetricKeyAlgorithm> encryptionAlgs = metadata.getEncryptionAlgorithms();
assertTrue(encryptionAlgs.hasNext(), "There is at least one EncryptedData child");
assertTrue(encryptionAlgs.hasNext(), "The child is still there");
assertEquals(SymmetricKeyAlgorithm.AES_128, encryptionAlgs.next(), "The first algo is AES128");
assertTrue(encryptionAlgs.hasNext(), "There is another EncryptedData");
assertTrue(encryptionAlgs.hasNext(), "There is *still* another EncryptedData");
assertEquals(SymmetricKeyAlgorithm.AES_256, encryptionAlgs.next(), "The second algo is AES256");
assertFalse(encryptionAlgs.hasNext(), "There is no more EncryptedData");
assertFalse(encryptionAlgs.hasNext(), "There *still* is no more EncryptedData");
assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm(), "getCompressionAlgorithm() returns alg of outermost CompressedData");
Iterator<CompressionAlgorithm> compAlgs = metadata.getCompressionAlgorithms();
assertTrue(compAlgs.hasNext());
assertTrue(compAlgs.hasNext());
assertEquals(CompressionAlgorithm.ZIP, compAlgs.next());
assertFalse(compAlgs.hasNext());
assertFalse(compAlgs.hasNext());
assertEquals("", metadata.getFilename());
JUtils.assertDateEquals(new Date(0L), metadata.getModificationDate());
assertEquals(StreamEncoding.BINARY, metadata.getFormat());
}
@Test
public void testProcessLiteralDataMessage() {
MessageMetadata.LiteralData literalData = new MessageMetadata.LiteralData(
"collateral_murder.zip",
DateUtil.parseUTCDate("2010-04-05 10:12:03 UTC"),
StreamEncoding.BINARY);
MessageMetadata.Message message = new MessageMetadata.Message();
message.setChild(literalData);
MessageMetadata metadata = new MessageMetadata(message);
assertNull(metadata.getCompressionAlgorithm());
assertNull(metadata.getEncryptionAlgorithm());
assertEquals("collateral_murder.zip", metadata.getFilename());
assertEquals(DateUtil.parseUTCDate("2010-04-05 10:12:03 UTC"), metadata.getModificationDate());
assertEquals(StreamEncoding.BINARY, metadata.getFormat());
}
}

View file

@ -238,8 +238,10 @@ public class ModificationDetectionTests {
);
ByteArrayOutputStream out = new ByteArrayOutputStream();
assertThrows(ModificationDetectionException.class, () -> {
Streams.pipeAll(decryptionStream, out);
assertThrows(ModificationDetectionException.class, decryptionStream::close);
decryptionStream.close();
});
}
@TestTemplate
@ -269,8 +271,10 @@ public class ModificationDetectionTests {
);
ByteArrayOutputStream out = new ByteArrayOutputStream();
assertThrows(ModificationDetectionException.class, () -> {
Streams.pipeAll(decryptionStream, out);
assertThrows(ModificationDetectionException.class, decryptionStream::close);
decryptionStream.close();
});
}
@TestTemplate
@ -313,8 +317,10 @@ public class ModificationDetectionTests {
);
ByteArrayOutputStream out = new ByteArrayOutputStream();
assertThrows(ModificationDetectionException.class, () -> {
Streams.pipeAll(decryptionStream, out);
assertThrows(ModificationDetectionException.class, decryptionStream::close);
decryptionStream.close();
});
}
@TestTemplate
@ -344,8 +350,10 @@ public class ModificationDetectionTests {
);
ByteArrayOutputStream out = new ByteArrayOutputStream();
assertThrows(ModificationDetectionException.class, () -> {
Streams.pipeAll(decryptionStream, out);
assertThrows(ModificationDetectionException.class, decryptionStream::close);
decryptionStream.close();
});
}
@TestTemplate
@ -530,7 +538,7 @@ public class ModificationDetectionTests {
);
}
private PGPSecretKeyRingCollection getDecryptionKey() throws IOException, PGPException {
private PGPSecretKeyRingCollection getDecryptionKey() throws IOException {
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(keyAscii);
return new PGPSecretKeyRingCollection(Collections.singletonList(secretKeys));
}

View file

@ -0,0 +1,687 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.Iterator;
import java.util.stream.Stream;
import org.bouncycastle.bcpg.ArmoredInputStream;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.bcpg.CompressionAlgorithmTags;
import org.bouncycastle.openpgp.PGPCompressedDataGenerator;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPLiteralDataGenerator;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.util.io.Streams;
import org.junit.JUtils;
import org.junit.jupiter.api.Named;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.CompressionAlgorithm;
import org.pgpainless.algorithm.StreamEncoding;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.encryption_signing.EncryptionOptions;
import org.pgpainless.encryption_signing.EncryptionResult;
import org.pgpainless.encryption_signing.EncryptionStream;
import org.pgpainless.encryption_signing.ProducerOptions;
import org.pgpainless.encryption_signing.SigningOptions;
import org.pgpainless.exception.MalformedOpenPgpMessageException;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.util.ArmoredInputStreamFactory;
import org.pgpainless.util.Passphrase;
import org.pgpainless.util.Tuple;
public class OpenPgpMessageInputStreamTest {
public static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
"Version: PGPainless\n" +
"Comment: DA05 848F 37D4 68E6 F982 C889 7A70 1FC6 904D 3F4C\n" +
"Comment: Alice <alice@pgpainless.org>\n" +
"\n" +
"lFgEYxzSCBYJKwYBBAHaRw8BAQdAeJU8m4GOJb1eQgv/ryilFHRfNLTYFMNqL6zj\n" +
"r0vF7dsAAP42rAtngpJ6dZxoZlJX0Je65zk1VMPeTrXaWfPS2HSKBRGptBxBbGlj\n" +
"ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmMc0ggJEHpwH8aQTT9M\n" +
"FiEE2gWEjzfUaOb5gsiJenAfxpBNP0wCngECmwEFFgIDAQAECwkIBwUVCgkICwKZ\n" +
"AQAApZEBALUXHtvswPZG28YO+16Men6/fpk+scvqpNMnD4ty3IkAAPwK6TuXjNnZ\n" +
"0XuWdnilvLMV23Ai1d5g6em+lwLK5M2SApxdBGMc0ggSCisGAQQBl1UBBQEBB0D8\n" +
"mNUVX8y2MXFaSeFYqOTPFnGT7dgNVdn6yc0UtkkHOgMBCAcAAP9y9OtP4SX9voPb\n" +
"ID2u9PkJKgo4hTB8NK5LouGppdRtEBGriHUEGBYKAB0FAmMc0ggCngECmwwFFgID\n" +
"AQAECwkIBwUVCgkICwAKCRB6cB/GkE0/TAywAQDpZRJS/joFH4+xcwheqWfI7ay/\n" +
"WfojUoGQMYGnUjsgYwEAkceRUsgkqI0SVgYvuglfaQpZ9k2ns1mZGVLkXvu/yQyc\n" +
"WARjHNIIFgkrBgEEAdpHDwEBB0BGN9BybSOrj8B6gim1SjbB/IiqAshlzMDunVkQ\n" +
"X23npQABAJqvjOOY7qhBuTusC5/Q5+25iLrhMn4TI+LXlJHMVNOaE0OI1QQYFgoA\n" +
"fQUCYxzSCAKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmMc0ggACgkQ\n" +
"KALh4BJQXl6yTQD/dh0N5228Uwtu7XHy6dmpMRX62cac5tXQ9WaDzpy8STgBAMdn\n" +
"Mq948UOYEhdk/ZY2/hwux/4t+FHvqrXW8ziBe4cLAAoJEHpwH8aQTT9M1hQA/3Ms\n" +
"P3kzoed3VsWu1ZMr7dKEngbc6SoJ2XPayzN0QYJaAQCIY5NcT9mZF97HWV3Vgeum\n" +
"00sWMHXfkW3+nl5OpUZaDA==\n" +
"=THgv\n" +
"-----END PGP PRIVATE KEY BLOCK-----";
public static final String PLAINTEXT = "Hello, World!\n";
public static final String PASSPHRASE = "sw0rdf1sh";
public static final String LIT = "" +
"-----BEGIN PGP MESSAGE-----\n" +
"Version: PGPainless\n" +
"\n" +
"yxRiAAAAAABIZWxsbywgV29ybGQhCg==\n" +
"=WGju\n" +
"-----END PGP MESSAGE-----";
public static final String LIT_LIT = "" +
"-----BEGIN PGP MESSAGE-----\n" +
"Version: BCPG v1.71\n" +
"\n" +
"yxRiAAAAAABIZWxsbywgV29ybGQhCssUYgAAAAAASGVsbG8sIFdvcmxkIQo=\n" +
"=A91Q\n" +
"-----END PGP MESSAGE-----";
public static final String COMP_LIT = "" +
"-----BEGIN PGP MESSAGE-----\n" +
"Version: BCPG v1.71\n" +
"\n" +
"owE7LZLEAAIeqTk5+ToK4flFOSmKXAA=\n" +
"=ZYDg\n" +
"-----END PGP MESSAGE-----";
public static final String COMP = "" +
"-----BEGIN PGP MESSAGE-----\n" +
"Version: BCPG v1.71\n" +
"\n" +
"owEDAA==\n" +
"=MDzg\n" +
"-----END PGP MESSAGE-----";
public static final String COMP_COMP_LIT = "" +
"-----BEGIN PGP MESSAGE-----\n" +
"Version: BCPG v1.71\n" +
"\n" +
"owEBRwC4/6MDQlpoOTFBWSZTWVuW2KAAAAr3hGAQBABgBABAAIAWBJAAAAggADFM\n" +
"ABNBqBo00N6puqWR+TqInoXQ58XckU4UJBbltigA\n" +
"=K9Zl\n" +
"-----END PGP MESSAGE-----";
public static final String SIG_COMP_LIT = "" +
"-----BEGIN PGP MESSAGE-----\n" +
"Version: BCPG v1.71\n" +
"\n" +
"iHUEABYKACcFAmMc1i0JECgC4eASUF5eFiEEjN3RiJxCf/TyYOQjKALh4BJQXl4A\n" +
"AHkrAP98uPpqrgIix7epgL7MM1cjXXGSxqbDfXHwgptk1YGQlgD/fw89VGcXwFaI\n" +
"2k7kpXQYy/1BqnovM/jZ3X3mXhhTaAOjATstksQAAh6pOTn5Ogrh+UU5KYpcAA==\n" +
"=WKPn\n" +
"-----END PGP MESSAGE-----";
public static final String SENC_LIT = "" +
"-----BEGIN PGP MESSAGE-----\n" +
"Version: PGPainless\n" +
"\n" +
"jA0ECQMCuZ0qHNXWnGhg0j8Bdm1cxV65sYb7jDgb4rRMtdNpQ1dC4UpSYuk9YWS2\n" +
"DpNEijbX8b/P1UOK2kJczNDADMRegZuLEI+dNsBnJjk=\n" +
"=i4Y0\n" +
"-----END PGP MESSAGE-----";
public static final String PENC_COMP_LIT = "" +
"-----BEGIN PGP MESSAGE-----\n" +
"Version: PGPainless\n" +
"\n" +
"hF4Dyqa/GWUy6WsSAQdAuGt49sQwdAHH3jPx11V3wSh7Amur3TbnONiQYJmMo3Qw\n" +
"87yBnZCsaB7evxLBgi6PpF3tiytHM60xlrPeKKPpJhu60vNafRM2OOwqk7AdcZw4\n" +
"0kYBEhiioO2btSuafNrQEjYzAgC7K6l7aPCcQObNp4ofryXu1P5vN+vUZp357hyS\n" +
"6zZqP+0wJQ9yJZMvFTtFeSaSi0oMP2sb\n" +
"=LvRL\n" +
"-----END PGP MESSAGE-----";
public static final String OPS_LIT_SIG = "" +
"-----BEGIN PGP MESSAGE-----\n" +
"Version: PGPainless\n" +
"\n" +
"kA0DAAoWKALh4BJQXl4ByxRiAAAAAABIZWxsbywgV29ybGQhCoh1BAAWCgAnBQJj\n" +
"I3fSCRAoAuHgElBeXhYhBIzd0YicQn/08mDkIygC4eASUF5eAADLOgEA766VyMMv\n" +
"sxfQwQHly3T6ySHSNhYEpoyvdxVqhjBBR+EA/3i6C8lKFPPTh/PvTGbVFOl+eUSV\n" +
"I0w3c+BRY/pO0m4H\n" +
"=tkTV\n" +
"-----END PGP MESSAGE-----";
public static void main(String[] args) throws Exception {
// genLIT();
// genLIT_LIT();
// genCOMP_LIT();
// genCOMP();
// genCOMP_COMP_LIT();
// genKey();
// genSIG_LIT();
// genSENC_LIT();
genPENC_COMP_LIT();
// genOPS_LIT_SIG();
}
public static void genLIT() throws IOException {
ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out);
PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator();
OutputStream litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]);
litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8));
litOut.close();
armorOut.close();
}
public static void genLIT_LIT() throws IOException {
ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out);
PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator();
OutputStream litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]);
litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8));
litOut.close();
litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]);
litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8));
litOut.close();
armorOut.close();
}
public static void genCOMP_LIT() throws IOException {
ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out);
PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP);
OutputStream compOut = compGen.open(armorOut);
PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator();
OutputStream litOut = litGen.open(compOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]);
litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8));
litOut.close();
compOut.close();
armorOut.close();
}
public static void genCOMP() throws IOException {
ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out);
PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP);
OutputStream compOut = compGen.open(armorOut);
compOut.close();
armorOut.close();
}
public static void genCOMP_COMP_LIT() throws IOException {
ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out);
PGPCompressedDataGenerator compGen1 = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP);
OutputStream compOut1 = compGen1.open(armorOut);
PGPCompressedDataGenerator compGen2 = new PGPCompressedDataGenerator(CompressionAlgorithmTags.BZIP2);
OutputStream compOut2 = compGen2.open(compOut1);
PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator();
OutputStream litOut = litGen.open(compOut2, PGPLiteralDataGenerator.BINARY, "", PGPLiteralDataGenerator.NOW, new byte[1 << 9]);
litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8));
litOut.close();
compOut2.close();
compOut1.close();
armorOut.close();
}
public static void genKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException {
PGPainless.asciiArmor(
PGPainless.generateKeyRing().modernKeyRing("Alice <alice@pgpainless.org>"),
System.out);
}
public static void genSIG_COMP_LIT() throws PGPException, IOException {
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY);
ByteArrayOutputStream msgOut = new ByteArrayOutputStream();
EncryptionStream signer = PGPainless.encryptAndOrSign()
.onOutputStream(msgOut)
.withOptions(
ProducerOptions.sign(
SigningOptions.get()
.addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys)
).setAsciiArmor(false)
);
Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), signer);
signer.close();
EncryptionResult result = signer.getResult();
PGPSignature detachedSignature = result.getDetachedSignatures().get(result.getDetachedSignatures().keySet().iterator().next()).iterator().next();
ByteArrayOutputStream out = new ByteArrayOutputStream();
ArmoredOutputStream armorOut = new ArmoredOutputStream(out);
armorOut.flush();
detachedSignature.encode(armorOut);
armorOut.write(msgOut.toByteArray());
armorOut.close();
String armored = out.toString();
// CHECKSTYLE:OFF
System.out.println(armored
.replace("-----BEGIN PGP SIGNATURE-----\n", "-----BEGIN PGP MESSAGE-----\n")
.replace("-----END PGP SIGNATURE-----", "-----END PGP MESSAGE-----"));
// CHECKSTYLE:ON
}
public static void genSENC_LIT() throws PGPException, IOException {
EncryptionStream enc = PGPainless.encryptAndOrSign()
.onOutputStream(System.out)
.withOptions(ProducerOptions.encrypt(EncryptionOptions.get()
.addPassphrase(Passphrase.fromPassword(PASSPHRASE)))
.overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED));
enc.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8));
enc.close();
}
public static void genPENC_COMP_LIT() throws IOException, PGPException {
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY);
PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys);
EncryptionStream enc = PGPainless.encryptAndOrSign()
.onOutputStream(System.out)
.withOptions(ProducerOptions.encrypt(EncryptionOptions.get()
.addRecipient(cert))
.overrideCompressionAlgorithm(CompressionAlgorithm.ZLIB));
Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), enc);
enc.close();
}
public static void genOPS_LIT_SIG() throws PGPException, IOException {
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY);
EncryptionStream enc = PGPainless.encryptAndOrSign()
.onOutputStream(System.out)
.withOptions(ProducerOptions.sign(SigningOptions.get()
.addSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys))
.overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED));
Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), enc);
enc.close();
}
interface Processor {
Tuple<String, MessageMetadata> process(String armoredMessage, ConsumerOptions options) throws PGPException, IOException;
}
private static Stream<Arguments> provideMessageProcessors() {
return Stream.of(
Arguments.of(Named.of("read(buf,off,len)", (Processor) OpenPgpMessageInputStreamTest::processReadBuffered)),
Arguments.of(Named.of("read()", (Processor) OpenPgpMessageInputStreamTest::processReadSequential))
);
}
@ParameterizedTest(name = "Process LIT using {0}")
@MethodSource("provideMessageProcessors")
public void testProcessLIT(Processor processor) throws IOException, PGPException {
Tuple<String, MessageMetadata> result = processor.process(LIT, ConsumerOptions.get());
String plain = result.getA();
assertEquals(PLAINTEXT, plain);
MessageMetadata metadata = result.getB();
assertNull(metadata.getCompressionAlgorithm());
assertNull(metadata.getEncryptionAlgorithm());
assertEquals("", metadata.getFilename());
JUtils.assertDateEquals(new Date(0L), metadata.getModificationDate());
assertEquals(StreamEncoding.BINARY, metadata.getFormat());
assertTrue(metadata.getVerifiedInlineSignatures().isEmpty());
assertTrue(metadata.getRejectedInlineSignatures().isEmpty());
}
@ParameterizedTest(name = "Process LIT LIT using {0}")
@MethodSource("provideMessageProcessors")
public void testProcessLIT_LIT_fails(Processor processor) {
assertThrows(MalformedOpenPgpMessageException.class,
() -> processor.process(LIT_LIT, ConsumerOptions.get()));
}
@ParameterizedTest(name = "Process COMP(LIT) using {0}")
@MethodSource("provideMessageProcessors")
public void testProcessCOMP_LIT(Processor processor)
throws PGPException, IOException {
Tuple<String, MessageMetadata> result = processor.process(COMP_LIT, ConsumerOptions.get());
String plain = result.getA();
assertEquals(PLAINTEXT, plain);
MessageMetadata metadata = result.getB();
assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm());
assertTrue(metadata.getVerifiedInlineSignatures().isEmpty());
assertTrue(metadata.getRejectedInlineSignatures().isEmpty());
}
@ParameterizedTest(name = "Process COMP using {0}")
@MethodSource("provideMessageProcessors")
public void testProcessCOMP_fails(Processor processor) {
assertThrows(MalformedOpenPgpMessageException.class,
() -> processor.process(COMP, ConsumerOptions.get()));
}
@ParameterizedTest(name = "Process COMP(COMP(LIT)) using {0}")
@MethodSource("provideMessageProcessors")
public void testProcessCOMP_COMP_LIT(Processor processor)
throws PGPException, IOException {
Tuple<String, MessageMetadata> result = processor.process(COMP_COMP_LIT, ConsumerOptions.get());
String plain = result.getA();
assertEquals(PLAINTEXT, plain);
MessageMetadata metadata = result.getB();
assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm());
Iterator<CompressionAlgorithm> compressionAlgorithms = metadata.getCompressionAlgorithms();
assertEquals(CompressionAlgorithm.ZIP, compressionAlgorithms.next());
assertEquals(CompressionAlgorithm.BZIP2, compressionAlgorithms.next());
assertFalse(compressionAlgorithms.hasNext());
assertNull(metadata.getEncryptionAlgorithm());
assertTrue(metadata.getVerifiedInlineSignatures().isEmpty());
assertTrue(metadata.getRejectedInlineSignatures().isEmpty());
}
@ParameterizedTest(name = "Process SIG COMP(LIT) using {0}")
@MethodSource("provideMessageProcessors")
public void testProcessSIG_COMP_LIT(Processor processor)
throws PGPException, IOException {
PGPPublicKeyRing cert = PGPainless.extractCertificate(
PGPainless.readKeyRing().secretKeyRing(KEY));
Tuple<String, MessageMetadata> result = processor.process(SIG_COMP_LIT, ConsumerOptions.get()
.addVerificationCert(cert));
String plain = result.getA();
assertEquals(PLAINTEXT, plain);
MessageMetadata metadata = result.getB();
assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm());
assertNull(metadata.getEncryptionAlgorithm());
assertFalse(metadata.getVerifiedInlineSignatures().isEmpty());
assertTrue(metadata.getRejectedInlineSignatures().isEmpty());
}
@ParameterizedTest(name = "Process SENC(LIT) using {0}")
@MethodSource("provideMessageProcessors")
public void testProcessSENC_LIT(Processor processor)
throws PGPException, IOException {
Tuple<String, MessageMetadata> result = processor.process(SENC_LIT, ConsumerOptions.get()
.addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE)));
String plain = result.getA();
assertEquals(PLAINTEXT, plain);
MessageMetadata metadata = result.getB();
assertNull(metadata.getCompressionAlgorithm());
assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm());
assertTrue(metadata.getVerifiedInlineSignatures().isEmpty());
assertTrue(metadata.getRejectedInlineSignatures().isEmpty());
}
@ParameterizedTest(name = "Process PENC(COMP(LIT)) using {0}")
@MethodSource("provideMessageProcessors")
public void testProcessPENC_COMP_LIT(Processor processor)
throws IOException, PGPException {
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY);
Tuple<String, MessageMetadata> result = processor.process(PENC_COMP_LIT, ConsumerOptions.get()
.addDecryptionKey(secretKeys));
String plain = result.getA();
assertEquals(PLAINTEXT, plain);
MessageMetadata metadata = result.getB();
assertEquals(CompressionAlgorithm.ZLIB, metadata.getCompressionAlgorithm());
assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm());
assertTrue(metadata.getVerifiedInlineSignatures().isEmpty());
assertTrue(metadata.getRejectedInlineSignatures().isEmpty());
}
@ParameterizedTest(name = "Process OPS LIT SIG using {0}")
@MethodSource("provideMessageProcessors")
public void testProcessOPS_LIT_SIG(Processor processor)
throws IOException, PGPException {
PGPPublicKeyRing cert = PGPainless.extractCertificate(PGPainless.readKeyRing().secretKeyRing(KEY));
Tuple<String, MessageMetadata> result = processor.process(OPS_LIT_SIG, ConsumerOptions.get()
.addVerificationCert(cert));
String plain = result.getA();
assertEquals(PLAINTEXT, plain);
MessageMetadata metadata = result.getB();
assertNull(metadata.getEncryptionAlgorithm());
assertNull(metadata.getCompressionAlgorithm());
assertFalse(metadata.getVerifiedInlineSignatures().isEmpty());
assertTrue(metadata.getRejectedInlineSignatures().isEmpty());
}
String BOB_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
"Comment: Bob's OpenPGP Transferable Secret Key\n" +
"\n" +
"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\n" +
"=miES\n" +
"-----END PGP PRIVATE KEY BLOCK-----";
@ParameterizedTest(name = "Process PENC(OPS OPS LIT SIG SIG) using {0}")
@MethodSource("provideMessageProcessors")
public void testProcessPENC_OPS_OPS_LIT_SIG_SIG(Processor processor) throws IOException, PGPException {
String MSG = "-----BEGIN PGP MESSAGE-----\n" +
"\n" +
"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\n" +
"=mdYs\n" +
"-----END PGP MESSAGE-----\n";
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(BOB_KEY);
PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys);
Tuple<String, MessageMetadata> result = processor.process(MSG, ConsumerOptions.get()
.addVerificationCert(certificate)
.addDecryptionKey(secretKeys));
String plain = result.getA();
assertEquals("encrypt ∘ sign ∘ sign", plain);
MessageMetadata metadata = result.getB();
assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm());
assertNull(metadata.getCompressionAlgorithm());
assertFalse(metadata.getVerifiedInlineSignatures().isEmpty());
assertTrue(metadata.getRejectedInlineSignatures().isEmpty());
}
@ParameterizedTest(name = "Process PENC(OPS OPS OPS LIT SIG SIG SIG) using {0}")
@MethodSource("provideMessageProcessors")
public void testProcessOPS_OPS_OPS_LIT_SIG_SIG_SIG(Processor processor) throws IOException, PGPException {
String MSG = "-----BEGIN PGP MESSAGE-----\n" +
"\n" +
"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\n" +
"=JP9T\n" +
"-----END PGP MESSAGE-----";
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(BOB_KEY);
PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys);
Tuple<String, MessageMetadata> result = processor.process(MSG, ConsumerOptions.get()
.addVerificationCert(certificate)
.addDecryptionKey(secretKeys));
String plain = result.getA();
assertEquals("encrypt ∘ sign ∘ sign ∘ sign", plain);
MessageMetadata metadata = result.getB();
assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm());
assertNull(metadata.getCompressionAlgorithm());
assertFalse(metadata.getVerifiedInlineSignatures().isEmpty());
assertTrue(metadata.getRejectedInlineSignatures().isEmpty());
}
private static Tuple<String, MessageMetadata> processReadBuffered(String armoredMessage, ConsumerOptions options)
throws PGPException, IOException {
OpenPgpMessageInputStream in = get(armoredMessage, options);
ByteArrayOutputStream out = new ByteArrayOutputStream();
Streams.pipeAll(in, out);
in.close();
MessageMetadata metadata = in.getMetadata();
return new Tuple<>(out.toString(), metadata);
}
private static Tuple<String, MessageMetadata> processReadSequential(String armoredMessage, ConsumerOptions options)
throws PGPException, IOException {
OpenPgpMessageInputStream in = get(armoredMessage, options);
ByteArrayOutputStream out = new ByteArrayOutputStream();
int r;
while ((r = in.read()) != -1) {
out.write(r);
}
in.close();
MessageMetadata metadata = in.getMetadata();
return new Tuple<>(out.toString(), metadata);
}
private static OpenPgpMessageInputStream get(String armored, ConsumerOptions options)
throws IOException, PGPException {
ByteArrayInputStream bytesIn = new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8));
ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn);
OpenPgpMessageInputStream pgpIn = OpenPgpMessageInputStream.create(armorIn, options);
return pgpIn;
}
}

View file

@ -185,7 +185,7 @@ public class PreventDecryptionUsingNonEncryptionKeyTest {
decryptionStream.close();
OpenPgpMetadata metadata = decryptionStream.getResult();
assertEquals(metadata.getDecryptionKey(), new SubkeyIdentifier(secretKeys, secretKeys.getPublicKey().getKeyID()));
assertEquals(new SubkeyIdentifier(secretKeys, secretKeys.getPublicKey().getKeyID()), metadata.getDecryptionKey());
}
@Test

View file

@ -11,12 +11,12 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.util.io.Streams;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
import org.pgpainless.PGPainless;
import org.pgpainless.exception.MalformedOpenPgpMessageException;
import org.pgpainless.util.TestAllImplementations;
public class RecursionDepthTest {
@ -143,7 +143,7 @@ public class RecursionDepthTest {
"-----END PGP ARMORED FILE-----\n";
assertThrows(PGPException.class, () -> {
assertThrows(MalformedOpenPgpMessageException.class, () -> {
DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify()
.onInputStream(new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8)))
.withOptions(new ConsumerOptions().addDecryptionKey(secretKey));

View file

@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.util.io.Streams;
import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless;
import org.pgpainless.encryption_signing.EncryptionOptions;
import org.pgpainless.encryption_signing.EncryptionStream;
import org.pgpainless.encryption_signing.ProducerOptions;
import org.gnupg.GnuPGDummyKeyUtil;
import org.pgpainless.exception.MissingDecryptionMethodException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class TryDecryptWithUnavailableGnuDummyKeyTest {
@Test
public void testAttemptToDecryptWithRemovedPrivateKeysThrows()
throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException {
PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing()
.modernKeyRing("Hardy Hardware <hardy@hard.ware>");
PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys);
ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream();
EncryptionStream encryptionStream = PGPainless.encryptAndOrSign()
.onOutputStream(ciphertextOut)
.withOptions(
ProducerOptions.encrypt(EncryptionOptions.get().addRecipient(certificate)));
ByteArrayInputStream plaintextIn = new ByteArrayInputStream("Hello, World!\n".getBytes());
Streams.pipeAll(plaintextIn, encryptionStream);
encryptionStream.close();
PGPSecretKeyRing removedKeys = GnuPGDummyKeyUtil.modify(secretKeys)
.removePrivateKeys(GnuPGDummyKeyUtil.KeyFilter.any());
ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ciphertextOut.toByteArray());
assertThrows(MissingDecryptionMethodException.class, () -> PGPainless.decryptAndOrVerify()
.onInputStream(ciphertextIn)
.withOptions(ConsumerOptions.get().addDecryptionKey(removedKeys)));
}
}

View file

@ -0,0 +1,410 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.util.io.Streams;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless;
public class UnsupportedPacketVersionsTest {
private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
"Comment: Bob's OpenPGP Transferable Secret Key\n" +
"\n" +
"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\n" +
"=miES\n" +
"-----END PGP PRIVATE KEY BLOCK-----\n";
private static final String PKESK3_PKESK23_SEIP = "-----BEGIN PGP MESSAGE-----\n" +
"\n" +
"wcDMA3wvqk35PDeyAQv/YP+fDWtifT7KSk+tWrgbyvsYCt5Wh0IPESTZuiptwvto\n" +
"CGbOfwuPbqqzzlFqSvX3UiJwhxjSSB3a1EBIOsbhc4grip/wm+fB50S/nTJxkJ14\n" +
"qid40D7HOcIvuz1iQr1QoMNB0oT3nCwMec8mPUX2yOzx1eqr62SZUTCr6FdAmdYI\n" +
"1u4EAeEFhRO0rcPRrpMZqwkXtUfx+pu7OzBS0qmOlfkQ50kbETDXBik4iXi30AGl\n" +
"Ifo792oRo6DFK7ENquTNRqFPfezjrGZfkJrPWulWh28GogWTpOBwfXG8X262QoIp\n" +
"VwZygi7wfj1jh2sXPvWgHjsjjTt7HPAiLI1f6IUl8WCQfPuQkFwCwPv63/rve59v\n" +
"sBaeCEykAxdzMbP1oYSBBtONSAPYW9fsUsJSpuuLvxH252+luk09uQXWd6z4aCDm\n" +
"EXiolhbkzL3mXCpVP6nMjRkm2ERE1yAWgXGT9JON0gcCb3eVqw6wzOYu+Vwq70ND\n" +
"vKYlTMY+9RUx7wLn51UgwUoXQUFBQUFBQUEJYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" +
"YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYdJMAQZo\n" +
"tUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNVCHg5QDO4\n" +
"wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" +
"=VS1M\n" +
"-----END PGP MESSAGE-----";
private static final String PKESK23_PKESK3_SEIP = "-----BEGIN PGP MESSAGE-----\n" +
"\n" +
"wUoXQUFBQUFBQUEJYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" +
"YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYcHAzAN8L6pN+Tw3sgEL/2D/nw1r\n" +
"Yn0+ykpPrVq4G8r7GAreVodCDxEk2boqbcL7aAhmzn8Lj26qs85Rakr191IicIcY\n" +
"0kgd2tRASDrG4XOIK4qf8JvnwedEv50ycZCdeKoneNA+xznCL7s9YkK9UKDDQdKE\n" +
"95wsDHnPJj1F9sjs8dXqq+tkmVEwq+hXQJnWCNbuBAHhBYUTtK3D0a6TGasJF7VH\n" +
"8fqbuzswUtKpjpX5EOdJGxEw1wYpOIl4t9ABpSH6O/dqEaOgxSuxDarkzUahT33s\n" +
"46xmX5Caz1rpVodvBqIFk6TgcH1xvF9utkKCKVcGcoIu8H49Y4drFz71oB47I407\n" +
"exzwIiyNX+iFJfFgkHz7kJBcAsD7+t/673ufb7AWnghMpAMXczGz9aGEgQbTjUgD\n" +
"2FvX7FLCUqbri78R9udvpbpNPbkF1nes+Ggg5hF4qJYW5My95lwqVT+pzI0ZJthE\n" +
"RNcgFoFxk/STjdIHAm93lasOsMzmLvlcKu9DQ7ymJUzGPvUVMe8C5+dVINJMAQZo\n" +
"tUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNVCHg5QDO4\n" +
"wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" +
"=EhNy\n" +
"-----END PGP MESSAGE-----\n";
private static final String PKESK3_SKESK23_SEIP = "-----BEGIN PGP MESSAGE-----\n" +
"\n" +
"wcDMA3wvqk35PDeyAQv/YP+fDWtifT7KSk+tWrgbyvsYCt5Wh0IPESTZuiptwvto\n" +
"CGbOfwuPbqqzzlFqSvX3UiJwhxjSSB3a1EBIOsbhc4grip/wm+fB50S/nTJxkJ14\n" +
"qid40D7HOcIvuz1iQr1QoMNB0oT3nCwMec8mPUX2yOzx1eqr62SZUTCr6FdAmdYI\n" +
"1u4EAeEFhRO0rcPRrpMZqwkXtUfx+pu7OzBS0qmOlfkQ50kbETDXBik4iXi30AGl\n" +
"Ifo792oRo6DFK7ENquTNRqFPfezjrGZfkJrPWulWh28GogWTpOBwfXG8X262QoIp\n" +
"VwZygi7wfj1jh2sXPvWgHjsjjTt7HPAiLI1f6IUl8WCQfPuQkFwCwPv63/rve59v\n" +
"sBaeCEykAxdzMbP1oYSBBtONSAPYW9fsUsJSpuuLvxH252+luk09uQXWd6z4aCDm\n" +
"EXiolhbkzL3mXCpVP6nMjRkm2ERE1yAWgXGT9JON0gcCb3eVqw6wzOYu+Vwq70ND\n" +
"vKYlTMY+9RUx7wLn51Ugw00XCQMINQp7MFzAc6T/YWFhYWFhYWFhYWFhYWFhYWFh\n" +
"YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYdJM\n" +
"AQZotUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNVCHg5\n" +
"QDO4wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" +
"=pvWj\n" +
"-----END PGP MESSAGE-----\n";
private static final String SKESK23_PKESK3_SEIP = "-----BEGIN PGP MESSAGE-----\n" +
"\n" +
"w00XCQMINQp7MFzAc6T/YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" +
"YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYcHAzAN8L6pN+Tw3sgEL/2D/\n" +
"nw1rYn0+ykpPrVq4G8r7GAreVodCDxEk2boqbcL7aAhmzn8Lj26qs85Rakr191Ii\n" +
"cIcY0kgd2tRASDrG4XOIK4qf8JvnwedEv50ycZCdeKoneNA+xznCL7s9YkK9UKDD\n" +
"QdKE95wsDHnPJj1F9sjs8dXqq+tkmVEwq+hXQJnWCNbuBAHhBYUTtK3D0a6TGasJ\n" +
"F7VH8fqbuzswUtKpjpX5EOdJGxEw1wYpOIl4t9ABpSH6O/dqEaOgxSuxDarkzUah\n" +
"T33s46xmX5Caz1rpVodvBqIFk6TgcH1xvF9utkKCKVcGcoIu8H49Y4drFz71oB47\n" +
"I407exzwIiyNX+iFJfFgkHz7kJBcAsD7+t/673ufb7AWnghMpAMXczGz9aGEgQbT\n" +
"jUgD2FvX7FLCUqbri78R9udvpbpNPbkF1nes+Ggg5hF4qJYW5My95lwqVT+pzI0Z\n" +
"JthERNcgFoFxk/STjdIHAm93lasOsMzmLvlcKu9DQ7ymJUzGPvUVMe8C5+dVINJM\n" +
"AQZotUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNVCHg5\n" +
"QDO4wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" +
"=STOd\n" +
"-----END PGP MESSAGE-----\n";
private static final String PKESK3_SKESK4wS2K23_SEIP = "-----BEGIN PGP MESSAGE-----\n" +
"\n" +
"wcDMA3wvqk35PDeyAQv/YP+fDWtifT7KSk+tWrgbyvsYCt5Wh0IPESTZuiptwvto\n" +
"CGbOfwuPbqqzzlFqSvX3UiJwhxjSSB3a1EBIOsbhc4grip/wm+fB50S/nTJxkJ14\n" +
"qid40D7HOcIvuz1iQr1QoMNB0oT3nCwMec8mPUX2yOzx1eqr62SZUTCr6FdAmdYI\n" +
"1u4EAeEFhRO0rcPRrpMZqwkXtUfx+pu7OzBS0qmOlfkQ50kbETDXBik4iXi30AGl\n" +
"Ifo792oRo6DFK7ENquTNRqFPfezjrGZfkJrPWulWh28GogWTpOBwfXG8X262QoIp\n" +
"VwZygi7wfj1jh2sXPvWgHjsjjTt7HPAiLI1f6IUl8WCQfPuQkFwCwPv63/rve59v\n" +
"sBaeCEykAxdzMbP1oYSBBtONSAPYW9fsUsJSpuuLvxH252+luk09uQXWd6z4aCDm\n" +
"EXiolhbkzL3mXCpVP6nMjRkm2ERE1yAWgXGT9JON0gcCb3eVqw6wzOYu+Vwq70ND\n" +
"vKYlTMY+9RUx7wLn51Ugw1AECRcIYWFhYWFhYWFBQUFBYWFhYWFhYWFhYWFhYWFh\n" +
"YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" +
"YdJMAQZotUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNV\n" +
"CHg5QDO4wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" +
"=/uxY\n" +
"-----END PGP MESSAGE-----\n";
private static final String SKESK4wS2K23_PKESK3_SEIP = "-----BEGIN PGP MESSAGE-----\n" +
"\n" +
"w1AECRcIYWFhYWFhYWFBQUFBYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" +
"YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYcHAzAN8L6pN+Tw3sgEL\n" +
"/2D/nw1rYn0+ykpPrVq4G8r7GAreVodCDxEk2boqbcL7aAhmzn8Lj26qs85Rakr1\n" +
"91IicIcY0kgd2tRASDrG4XOIK4qf8JvnwedEv50ycZCdeKoneNA+xznCL7s9YkK9\n" +
"UKDDQdKE95wsDHnPJj1F9sjs8dXqq+tkmVEwq+hXQJnWCNbuBAHhBYUTtK3D0a6T\n" +
"GasJF7VH8fqbuzswUtKpjpX5EOdJGxEw1wYpOIl4t9ABpSH6O/dqEaOgxSuxDark\n" +
"zUahT33s46xmX5Caz1rpVodvBqIFk6TgcH1xvF9utkKCKVcGcoIu8H49Y4drFz71\n" +
"oB47I407exzwIiyNX+iFJfFgkHz7kJBcAsD7+t/673ufb7AWnghMpAMXczGz9aGE\n" +
"gQbTjUgD2FvX7FLCUqbri78R9udvpbpNPbkF1nes+Ggg5hF4qJYW5My95lwqVT+p\n" +
"zI0ZJthERNcgFoFxk/STjdIHAm93lasOsMzmLvlcKu9DQ7ymJUzGPvUVMe8C5+dV\n" +
"INJMAQZotUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNV\n" +
"CHg5QDO4wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" +
"=cIwV\n" +
"-----END PGP MESSAGE-----\n";
private static final String PKESK3_SEIP_OPS3_LIT_SIG4 = "-----BEGIN PGP MESSAGE-----\n" +
"\n" +
"wcDMA3wvqk35PDeyAQv+IhkCMhbdRcMnIPZNPGU6OK1Jk5xuRdIEIBsvv7b8jmAr\n" +
"9IwjfnV/RDMtH+xR/T9K7qJGGFYnhLY5w0CmYHQcDKpcBqk0Dw6l/eKCNhgRXKAk\n" +
"gfaKL1Utt1Pw0nz0mOwHyPEN/pGc0xlVhsjVkRvIOsKcpfuc1EpSZMFgDcBQDhe/\n" +
"jAsR/MvRugkW8xLpyQyfeGLJUOEYVrkpam3rLKB1KywAgBmpr9WDwfYITW/VE9k5\n" +
"cKIOPMDJFU+u9lzBx6nSS4JRBuCO2mhR4gjcRaGPWiiz+0qZfS+AXYV/MAU5OwK/\n" +
"6nhX97zwS4r1Avztjh4taBhLVsY4pw6PuLtACJNwPrev63Yc+a4hJJ4pm1AnHV58\n" +
"Y1pZKQL8vA61+/tbhFQ18vbJ8E1NOka/euFLQu9Mg58jhpcscqMouyr3JFMwgH2Y\n" +
"eFuRJncJAKotXxlfnF37qz5LG3bamACXWZSObjp9d4quIAoCDUteZlDWQ1xq5R4y\n" +
"QYXtk9ZuHsHsmY9A0CiI0sGYAUBtLubJ5qhLLr/GqKAmy8jTSA3MjgtrB55NWj9J\n" +
"bjgFNsd1BNGklgnwhtmApHJWY8skAAQkJj0rXj/aOMc734ypiEWDiU1quRbEeRLR\n" +
"kDvBNUXx2j2rVF+MmQS/sm5Yk/op+4lH/Wounsci3qWH76GaNZoIlvNE3mdFoVTe\n" +
"cRh4W2Em8uAUH4bKwazltRJUhZmXvuGfUnQCmolJTpyPl4DaQQgzdBXLTRcPxwdU\n" +
"30e7HnxZWESKx1LnGxp3Oan1k1lXyHwvnEk26EXhve+dhsQ6YsKgvtSLNFqGsfKe\n" +
"MVOq37cpOGFQsYStWHZd0tcHjIjWmAeZ8kH+ZzR9tgYKxxjimsxafLS/lo415SkC\n" +
"LnOCz6hywI7CufSUcXUlHGJuobZ5HDJcygsQhQmNVLDmKh4xUJDZrORS0ciMy2kc\n" +
"XAnxCDYbltVQktc/F/Gl1lTx0UNmV5d9G0utVmxxGbXna3BLkB+6qMuux/ngC+cI\n" +
"+0GjeuXDVzUqhKDDC3Sq4T2nwix4CjKvawnHC+vpHzRmdZSkXz3nrSdhJS5JnOap\n" +
"I4CdyYkP3jQs5P03dkv5RZAoqNPkeftwadu0cjZj8HEC8bVSd7YCS/0Gbgvp/eK3\n" +
"aOXZAWhELyQON4bbXiWzMTcO2AA5soBmP2QnBNdq7NxbEkBec8aAXZiyXn17JYit\n" +
"HRtJ7beptXjN0y2bEhOvsBHbDpjGO22fZfQ0aywP2k4/XanDf4WJolEFgLj2Qp8F\n" +
"pw1Olc2UhApxhAqjkme09hggli3wUhthQrpBlmntfbvBcmmO+p/jV8Zn\n" +
"=Roko\n" +
"-----END PGP MESSAGE-----\n";
private static final String PKESK3_SEIP_OPS23_LIT_SIG23 = "-----BEGIN PGP MESSAGE-----\n" +
"\n" +
"wcDMA3wvqk35PDeyAQv/byYGOehnfgZu/HJSSDEQhYE27lh5j+oQPktYBxsTI3Ii\n" +
"AJ0pyJggtUM3c/e6z4HioAs4oQiaH1eoGBIl7noROhHJgT6E/oKdHJHmQndo0gT7\n" +
"xzSHKrEZYEqT45LD6jT5teOiwGX/B7as6jaQOT+Nh+M0ZqpPrBdWDeUoY/I/lx9j\n" +
"IcXQUhKuqJwZ16xsnv0JJ80rdp5qv0g2NHT1hK1JEONyT2fefov3EXaSpQZHRBEi\n" +
"XfwToHcJrgemFoGwZBQhXsPWBgKH92aW6r7ZJZMQ4BE2SwqEw+cbaaqwfFRJ9puj\n" +
"ZUBi2JGwnYImZyD7jYverkjH05vI7d5qQDhCT6GPD8Q0WKfY225LFTj/zzbC23lp\n" +
"VLlbT2Ap8ZFOESpM+crOUaOguBhnTOF05s1eXhQYxIKzJTW8UVrzbwI8ut3BnEDj\n" +
"0aDqUR+QDYgYmz8hnEjanHk2McvfaNdV6uOPZNph2RuPzhZeoNcz1PLy+XPFJsQT\n" +
"EdqURJ2D35qmrBvq6klN0sGYAXEtW4hxMaKhH7/SIk2m3kUAChjtzr4XRcE2i8h3\n" +
"9uD6CRWxokArfRVAp5RpXt3ywoYrl1Mp5prVwWcrTzYVPZwwe/bYAFTIfavi0Ezb\n" +
"A/ah2e1EYCTWxLY0Klzil2Xw9/Dc/JPTRqzWJxIn0AU4DVfYNwlH3QimDUDKOKbu\n" +
"bw4bLEEBKRr5BcNMA2rJOw/n3AmKxQcdSFJh3ZNtDPWzRwflIzE2qB686hpeOs34\n" +
"T2iJcfr9W9rKcYI7+WYcZA3fWokaWUfXdWrPMXBVJuPdGezGLSfe/OM4kw/8s2vR\n" +
"Bk88WZ1ZiFXE2CRaHP80fHFpxAZioWTuC5UGhF7NgZ7Q1E85GaGVe1fQeqmeX3mo\n" +
"gwAWwq9WFhPQQPwdrDz+1h/pzD0RVW7D+zfWdF//vesc4z1Bpi5prbMdpVdmyvO9\n" +
"8Rcc42GtmhtYSB9SPjzPuN8PrWvD3AKgw5vQro6oiNh0TGmj5Se4lXCfRfCl4tak\n" +
"cmvdi+1wAvn5OFdxYKKHNvjavwj2SY70nx0ACasBpbMEwoQ0StZmOxCaofkgvEwN\n" +
"t8jMq/MVrIfunhMjx0/GDpBZBb8kze8zrvWxlbTnoIfh2yVEqmTZWue9HX5Mnh8P\n" +
"wexxNrPaafTjA3nUgXXzlItJ/Wa43pYw2sgcBPlF/jRKSFiD+pDLysqwpH0ANsJ3\n" +
"G7t8Qavq1DlrHFgV5jhfR1tjA5ohjxx7yzceQBvZUFxKM1WEWR+9dRb3bpfZJr0g\n" +
"qgO36bpCeCEej+ubXpXXTN28LQLXjQHlE2o1NGLoGl+G72tXOTx30kPS\n" +
"=wUZn\n" +
"-----END PGP MESSAGE-----\n";
private static final String PKESK3_SEIP_OPS23_OPS3_LIT_SIG4_SIG23 = "-----BEGIN PGP MESSAGE-----\n" +
"\n" +
"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\n" +
"=qFHk\n" +
"-----END PGP MESSAGE-----\n";
private static final String PKESK3_SEIP_OPS3_OPS23_LIT_SIG23_SIG4 = "-----BEGIN PGP MESSAGE-----\n" +
"\n" +
"wcDMA3wvqk35PDeyAQv+JdNtgn26/Q4yXJ9egCmJ1/3SaeSB+y93jz+fmabMvc97\n" +
"KbI1HN9RoG23UxDoQED+jGcbbrw+7ho73U3uRC4NtE4D4SZZMQZZXTadI0MEu4eN\n" +
"yGgyFmZLy2Fek5N33m4PShvKqbeDnubmxv/lPlpz//KuxbVNPL+vtiVxZuI6vusB\n" +
"q3T0Br0kH7OqCizCIKzl85d9hYAKXDaCaGw/0VXmFs0HgjBSB5RRHKt3GpVt83dI\n" +
"b7vyyrzo6D0bwr+9nYC9Q+rku0lPNJutdSBRv9Xoc8eB7ud+0x9Ybj61C91gjvJp\n" +
"NkeuqcCYOZw+iFckDR8+GUM4T5PO3dwxEVQUUeO99EKuCH2S/PqgrH8JFiRcCvED\n" +
"SsngpAL3IfkoKCuAO0gpK4ebDKQ8R46ho/ER1UApT9A8lNbzLYIwCQ8fcK53AICe\n" +
"XWDTGb1uqqkt0vFPxsKA0R6Wyk6gA8j8ta6zpgTpwGOyRbMKo8QW08mw+2UROCPj\n" +
"/cdtV1Z6Iv0GrbKwrwB20sOlAYDKbSloZBhkwugYMqYWQfzPcM8S+sCX/+Kv/O79\n" +
"oWucvbSAiUv0JlD6zqdNcileqJKCiiAVaCUZhLpZcgVWpLqfJuFFnHBOo98ARIHe\n" +
"D5CzXn2sx+0ZlFW0fJk8Z2ZWXK9rKAleqsGB4dIA3WoC4UAFFjBqXG/4pa/H0He2\n" +
"G8R3Q+wFqEaXYgm2Znq/+UxPGjAJLH7EUrwfBvK17eByT+bLqyZpKHhuZJXy/rw1\n" +
"n+pCC1fedDWdxKj7+1Xw8cVlAWYCp2314DQjfI8BzFw0JWq8MU7hwGDQkJgfI3qH\n" +
"RlBhIgE2iJAOQi4YaSgC9QaAxL4Uw3uo9+kwUGt87j+M4d7ALQ1XXLtCim6P36jP\n" +
"kjOoAvfgwZ1NZEbzo/YS9/NK9KVXrhfgmkWSPjLaqJeur2Av9IkDuQmFF+EVxjRo\n" +
"eQzZHk8RHOj03jjTZH/QHNiiUDfr2cMDj8Hi+r5pCvS/T67gymyE6VHQJSYS9dXP\n" +
"0EMoe4jaG+aOdbAi3OPtKETSvtsKLflMR4k/RxRXN6lsV238wVna+w6nYZxqMqd4\n" +
"fNnL5YUHZOEt2qxVguOCEZDANHoR0RFVXM6yBF36Ivwjhg5a1aujyHv150KwDDsc\n" +
"YMI4O2pTcmhDX1aiV2X35EyLWJbSovbNj/IveMKa0q/xOXe4V8INX9Xv4sxm0mqY\n" +
"RR8CY1E3cYG2g6Uhc4WkirSvXoN/IRq1MrYmcCHQrEDDBL5h/8/TIn/TAOZqBrZ/\n" +
"gF1gfFW4ZgEcPZUeUmErHxLvdVqC+WK7/5qE86PXGo9yD9/Xxv5U7i8BbxgknUlD\n" +
"SyRmBfzkRJudTHvH9wnk9KA5hPHqXk6ZkrBo4ugaiwUa/EejvkiHW7KRWijH57nL\n" +
"JDzP13FU16gPuhPFTXP2zLvLeMSpVmkv2B9Mzvnrg+836B+hY0elAy5U/3D1/wxG\n" +
"IjfDTsAcUQ8yULCRj6iZrx1SZc0HJDe4mjvqVqg0VOSpxGHfRNcte+zh6p8Fqqly\n" +
"2O72vb49uEr54ZeL33j/ggCXWvMgdK7EtIirmzcwFsmamy89QSl1VzZzh5338n7f\n" +
"9SOd67xL8BVSh8lee8ByuBiZryLbIuy1d8stndbbxLi+W7+Y/W08g3QyE+NwcpKd\n" +
"/zTVViCZolgs4Ol73WEe6A131u+AMlJWXYD5tai+RmQOFugvCVX+QhezK1v3YrMH\n" +
"KlIfFsh4Cq+JIo2jMMoVjLBK662kU24w8eaEagdIjBgd1XlEBgKUR/f754BOfoKi\n" +
"JX2ySeHdQCCn/yc753X1TH3FNEThmJPHJG0ESkpIxqoTKdL3Ut+8BFlhWYwxCc8r\n" +
"R8m9ixq0cQBXrNVaJsFVKqI9H4SJMc8ySGe8HYwJV2hhK9HbuhAfrKiJoUmoQHvD\n" +
"jL9Y6H3ejK5YmqQ/zXoiepRfAklN3q+ByqhRMjZfDMuk0fcMaPy9RFoo1FqIPyqw\n" +
"alekNaR/K4albyRcMoYxBhn3QFHf7VuaPuaxhg1ri3YfrWykv3RA\n" +
"=jGpv\n" +
"-----END PGP MESSAGE-----\n";
private final PGPSecretKeyRing key;
private final PGPPublicKeyRing cert;
public UnsupportedPacketVersionsTest() throws IOException {
key = PGPainless.readKeyRing().secretKeyRing(KEY);
cert = PGPainless.extractCertificate(key);
}
@Test
public void pkesk3_pkesk23_seip() throws PGPException, IOException {
decryptAndCompare(PKESK3_PKESK23_SEIP, "Encrypted using SEIP + MDC.");
}
@Test
public void pkesk23_pkesk_seip() throws PGPException, IOException {
decryptAndCompare(PKESK23_PKESK3_SEIP, "Encrypted using SEIP + MDC.");
}
@Test
public void pkesk3_skesk23_seip() throws PGPException, IOException {
decryptAndCompare(PKESK3_SKESK23_SEIP, "Encrypted using SEIP + MDC.");
}
@Test
public void skesk23_pkesk3_seip() throws PGPException, IOException {
decryptAndCompare(SKESK23_PKESK3_SEIP, "Encrypted using SEIP + MDC.");
}
@Test
@Disabled("Enable once https://github.com/bcgit/bc-java/pull/1268 is available")
public void pkesk3_skesk4Ws2k23_seip() throws PGPException, IOException {
decryptAndCompare(PKESK3_SKESK4wS2K23_SEIP, "Encrypted using SEIP + MDC.");
}
@Test
@Disabled("Enable once https://github.com/bcgit/bc-java/pull/1268 is available")
public void skesk4Ws2k23_pkesk3_seip() throws PGPException, IOException {
decryptAndCompare(SKESK4wS2K23_PKESK3_SEIP, "Encrypted using SEIP + MDC.");
}
@Test
public void pkesk3_seip_ops3_lit_sig4() throws PGPException, IOException {
decryptAndCompare(PKESK3_SEIP_OPS3_LIT_SIG4, "Encrypted, signed message.");
}
@Test
public void pkesk3_seip_ops23_lit_sig23() throws PGPException, IOException {
decryptAndCompare(PKESK3_SEIP_OPS23_LIT_SIG23, "Encrypted, signed message.");
}
@Test
public void pkesk3_seip_ops23_ops3_lit_sig4_sig23() throws PGPException, IOException {
decryptAndCompare(PKESK3_SEIP_OPS23_OPS3_LIT_SIG4_SIG23, "Encrypted, signed message.");
}
@Test
public void pkesk3_seip_ops3_ops23_lit_sig23_sig4() throws PGPException, IOException {
decryptAndCompare(PKESK3_SEIP_OPS3_OPS23_LIT_SIG23_SIG4, "Encrypted, signed message.");
}
public void decryptAndCompare(String msg, String plain) throws IOException, PGPException {
// noinspection CharsetObjectCanBeUsed
ByteArrayInputStream inputStream = new ByteArrayInputStream(msg.getBytes(Charset.forName("UTF8")));
DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify()
.onInputStream(inputStream)
.withOptions(ConsumerOptions.get()
.addDecryptionKey(key)
.addVerificationCert(cert));
ByteArrayOutputStream out = new ByteArrayOutputStream();
Streams.pipeAll(decryptionStream, out);
decryptionStream.close();
assertEquals(plain, out.toString());
}
}

View file

@ -0,0 +1,197 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification.syntax_check;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
import org.pgpainless.exception.MalformedOpenPgpMessageException;
public class PDATest {
/**
* MSG is valid.
*
* @throws MalformedOpenPgpMessageException fail
*/
@Test
public void testSimpleLiteralMessageIsValid() throws MalformedOpenPgpMessageException {
PDA check = new PDA();
check.next(InputSymbol.LiteralData);
check.next(InputSymbol.EndOfSequence);
assertTrue(check.isValid());
}
/**
* OPS MSG SIG is valid.
*
* @throws MalformedOpenPgpMessageException fail
*/
@Test
public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException {
PDA check = new PDA();
check.next(InputSymbol.OnePassSignature);
check.next(InputSymbol.LiteralData);
check.next(InputSymbol.Signature);
check.next(InputSymbol.EndOfSequence);
assertTrue(check.isValid());
}
/**
* SIG MSG is valid.
*
* @throws MalformedOpenPgpMessageException fail
*/
@Test
public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException {
PDA check = new PDA();
check.next(InputSymbol.Signature);
check.next(InputSymbol.LiteralData);
check.next(InputSymbol.EndOfSequence);
assertTrue(check.isValid());
}
/**
* OPS COMP(MSG) SIG is valid.
*
* @throws MalformedOpenPgpMessageException fail
*/
@Test
public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException {
PDA check = new PDA();
check.next(InputSymbol.OnePassSignature);
check.next(InputSymbol.CompressedData);
// Here would be a nested PDA for the LiteralData packet
check.next(InputSymbol.Signature);
check.next(InputSymbol.EndOfSequence);
assertTrue(check.isValid());
}
@Test
public void testOPSSignedEncryptedMessageIsValid() {
PDA check = new PDA();
check.next(InputSymbol.OnePassSignature);
check.next(InputSymbol.EncryptedData);
check.next(InputSymbol.Signature);
check.next(InputSymbol.EndOfSequence);
assertTrue(check.isValid());
}
@Test
public void anyInputAfterEOSIsNotValid() {
PDA check = new PDA();
check.next(InputSymbol.LiteralData);
check.next(InputSymbol.EndOfSequence);
assertThrows(MalformedOpenPgpMessageException.class,
() -> check.next(InputSymbol.Signature));
}
@Test
public void testEncryptedMessageWithAppendedStandaloneSigIsNotValid() {
PDA check = new PDA();
check.next(InputSymbol.EncryptedData);
assertThrows(MalformedOpenPgpMessageException.class,
() -> check.next(InputSymbol.Signature));
}
@Test
public void testOPSSignedEncryptedMessageWithMissingSigIsNotValid() {
PDA check = new PDA();
check.next(InputSymbol.OnePassSignature);
check.next(InputSymbol.EncryptedData);
assertThrows(MalformedOpenPgpMessageException.class,
() -> check.next(InputSymbol.EndOfSequence));
}
@Test
public void testTwoLiteralDataIsNotValid() {
PDA check = new PDA();
check.next(InputSymbol.LiteralData);
assertThrows(MalformedOpenPgpMessageException.class,
() -> check.next(InputSymbol.LiteralData));
}
@Test
public void testTrailingSigIsNotValid() {
PDA check = new PDA();
check.next(InputSymbol.LiteralData);
assertThrows(MalformedOpenPgpMessageException.class,
() -> check.next(InputSymbol.Signature));
}
@Test
public void testOPSAloneIsNotValid() {
PDA check = new PDA();
check.next(InputSymbol.OnePassSignature);
assertThrows(MalformedOpenPgpMessageException.class,
() -> check.next(InputSymbol.EndOfSequence));
}
@Test
public void testOPSLitWithMissingSigIsNotValid() {
PDA check = new PDA();
check.next(InputSymbol.OnePassSignature);
check.next(InputSymbol.LiteralData);
assertThrows(MalformedOpenPgpMessageException.class,
() -> check.next(InputSymbol.EndOfSequence));
}
@Test
public void testCompressedMessageWithStandalongAppendedSigIsNotValid() {
PDA check = new PDA();
check.next(InputSymbol.CompressedData);
assertThrows(MalformedOpenPgpMessageException.class,
() -> check.next(InputSymbol.Signature));
}
@Test
public void testOPSCompressedDataWithMissingSigIsNotValid() {
PDA check = new PDA();
check.next(InputSymbol.OnePassSignature);
check.next(InputSymbol.CompressedData);
assertThrows(MalformedOpenPgpMessageException.class,
() -> check.next(InputSymbol.EndOfSequence));
}
@Test
public void testCompressedMessageFollowedByTrailingLiteralDataIsNotValid() {
PDA check = new PDA();
check.next(InputSymbol.CompressedData);
assertThrows(MalformedOpenPgpMessageException.class,
() -> check.next(InputSymbol.LiteralData));
}
@Test
public void testOPSWithPrependedSigIsValid() {
PDA check = new PDA();
check.next(InputSymbol.Signature);
check.next(InputSymbol.OnePassSignature);
check.next(InputSymbol.LiteralData);
check.next(InputSymbol.Signature);
check.next(InputSymbol.EndOfSequence);
assertTrue(check.isValid());
}
@Test
public void testPrependedSigInsideOPSSignedMessageIsValid() {
PDA check = new PDA();
check.next(InputSymbol.OnePassSignature);
check.next(InputSymbol.Signature);
check.next(InputSymbol.LiteralData);
check.next(InputSymbol.Signature);
check.next(InputSymbol.EndOfSequence);
assertTrue(check.isValid());
}
}

View file

@ -9,7 +9,6 @@ import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
@ -165,7 +164,7 @@ public class EncryptionOptionsTest {
}
@Test
public void testAddRecipients_PGPPublicKeyRingCollection() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException {
public void testAddRecipients_PGPPublicKeyRingCollection() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException {
PGPPublicKeyRing secondKeyRing = KeyRingUtils.publicKeyRingFrom(
PGPainless.generateKeyRing().modernKeyRing("other@pgpainless.org"));

View file

@ -13,7 +13,7 @@ import org.pgpainless.PGPainless;
* This class contains a set of slightly out of spec or weird keys.
* Those are used to test whether implementations behave correctly when dealing with such keys.
*
* Original source: https://gitlab.com/sequoia-pgp/weird-keys
* @see <a href="https://gitlab.com/sequoia-pgp/weird-keys">Original Source</a>
*/
public class WeirdKeys {

View file

@ -140,7 +140,7 @@ public class CertifyCertificateTest {
}
@Test
public void testScopedDelegation() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException {
public void testScopedDelegation() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException {
PGPSecretKeyRing aliceKey = PGPainless.generateKeyRing()
.modernKeyRing("Alice <alice@pgpainless.org>");
PGPSecretKeyRing caKey = PGPainless.generateKeyRing()

View file

@ -7,7 +7,6 @@ package org.pgpainless.key.generation;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
@ -26,14 +25,15 @@ import org.pgpainless.key.generation.type.rsa.RsaLength;
import org.pgpainless.key.util.KeyRingUtils;
import org.pgpainless.key.util.UserId;
import org.pgpainless.timeframe.TestTimeFrameProvider;
import org.pgpainless.util.DateUtil;
import org.pgpainless.util.TestAllImplementations;
public class GenerateKeyWithAdditionalUserIdTest {
@TestTemplate
@ExtendWith(TestAllImplementations.class)
public void test() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException {
Date now = new Date();
public void test() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException {
Date now = DateUtil.now();
Date expiration = TestTimeFrameProvider.defaultExpirationForCreationDate(now);
PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing()
.setPrimaryKey(KeySpec.getBuilder(

View file

@ -20,7 +20,7 @@ import org.pgpainless.util.TestAllImplementations;
import org.pgpainless.util.Passphrase;
/**
* Reproduce behavior of https://github.com/pgpainless/pgpainless/issues/16
* Reproduce behavior of <a href="https://github.com/pgpainless/pgpainless/issues/16>this issue</a>
* and verify that the fix is working.
*
* The issue is that the implementation of {@link Passphrase#emptyPassphrase()} would set the underlying

View file

@ -20,6 +20,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.util.io.Streams;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless;
import org.pgpainless.decryption_verification.ConsumerOptions;
@ -159,6 +160,8 @@ public class IgnoreMarkerPackets {
}
@Test
@Disabled
// TODO: Enable and fix
public void markerPlusEncryptedMessage() throws IOException, PGPException {
String msg = "-----BEGIN PGP MESSAGE-----\n" +
"\n" +

View file

@ -8,7 +8,6 @@ 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.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
@ -19,6 +18,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.bouncycastle.util.Arrays;
import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.KeyFlag;
@ -35,8 +35,7 @@ public class BCUtilTest {
@Test
public void keyRingToCollectionTest()
throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException,
IOException {
throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException {
PGPSecretKeyRing sec = PGPainless.buildKeyRing()
.setPrimaryKey(KeySpec.getBuilder(
KeyType.RSA(RsaLength._3072),
@ -94,14 +93,14 @@ public class BCUtilTest {
@Test
public void constantTimeAreEqualsTest() {
char[] b = "Hello".toCharArray();
assertTrue(BCUtil.constantTimeAreEqual(b, b));
assertTrue(BCUtil.constantTimeAreEqual("Hello".toCharArray(), "Hello".toCharArray()));
assertTrue(BCUtil.constantTimeAreEqual(new char[0], new char[0]));
assertTrue(BCUtil.constantTimeAreEqual(new char[] {'H', 'e', 'l', 'l', 'o'}, "Hello".toCharArray()));
assertTrue(Arrays.constantTimeAreEqual(b, b));
assertTrue(Arrays.constantTimeAreEqual("Hello".toCharArray(), "Hello".toCharArray()));
assertTrue(Arrays.constantTimeAreEqual(new char[0], new char[0]));
assertTrue(Arrays.constantTimeAreEqual(new char[] {'H', 'e', 'l', 'l', 'o'}, "Hello".toCharArray()));
assertFalse(BCUtil.constantTimeAreEqual("Hello".toCharArray(), "Hello World".toCharArray()));
assertFalse(BCUtil.constantTimeAreEqual(null, "Hello".toCharArray()));
assertFalse(BCUtil.constantTimeAreEqual("Hello".toCharArray(), null));
assertFalse(BCUtil.constantTimeAreEqual(null, null));
assertFalse(Arrays.constantTimeAreEqual("Hello".toCharArray(), "Hello World".toCharArray()));
assertFalse(Arrays.constantTimeAreEqual(null, "Hello".toCharArray()));
assertFalse(Arrays.constantTimeAreEqual("Hello".toCharArray(), null));
assertFalse(Arrays.constantTimeAreEqual((char[]) null, null));
}
}

View file

@ -56,7 +56,7 @@ public class KeyRingsFromCollectionTest {
}
@Test
public void selectPublicKeyRingFromPublicKeyRingCollectionTest() throws IOException, PGPException {
public void selectPublicKeyRingFromPublicKeyRingCollectionTest() throws IOException {
PGPPublicKeyRing emil = TestKeys.getEmilPublicKeyRing();
PGPPublicKeyRing juliet = TestKeys.getJulietPublicKeyRing();
PGPPublicKeyRingCollection collection = new PGPPublicKeyRingCollection(Arrays.asList(emil, juliet));
@ -68,7 +68,7 @@ public class KeyRingsFromCollectionTest {
}
@Test
public void selectPublicKeyRingMapFromPublicKeyRingCollectionMapTest() throws IOException, PGPException {
public void selectPublicKeyRingMapFromPublicKeyRingCollectionMapTest() throws IOException {
PGPPublicKeyRing emil = TestKeys.getEmilPublicKeyRing();
PGPPublicKeyRing juliet = TestKeys.getJulietPublicKeyRing();
MultiMap<String, PGPPublicKeyRingCollection> map = new MultiMap<>();

View file

@ -19,7 +19,7 @@ SPDX-License-Identifier: Apache-2.0
</encoder>
</appender>
<logger name="org.pgpainless" level="warn" additivity="false">
<logger name="org.pgpainless" level="debug" additivity="false">
<appender-ref ref="STDOUT" />
</logger>

View file

@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your
...
dependencies {
...
implementation "org.pgpainless:pgpainless-sop:1.3.8"
implementation "org.pgpainless:pgpainless-sop:1.4.0-rc1"
...
}
@ -34,7 +34,7 @@ dependencies {
<dependency>
<groupId>org.pgpainless</groupId>
<artifactId>pgpainless-sop</artifactId>
<version>1.3.8</version>
<version>1.4.0-rc1</version>
</dependency>
...
</dependencies>

View file

@ -18,7 +18,7 @@ import sop.operation.Dearmor;
public class DearmorImpl implements Dearmor {
@Override
public Ready data(InputStream data) throws IOException {
public Ready data(InputStream data) {
InputStream decoder;
try {
decoder = PGPUtil.getDecoderStream(data);

View file

@ -23,6 +23,7 @@ import org.pgpainless.decryption_verification.ConsumerOptions;
import org.pgpainless.decryption_verification.DecryptionStream;
import org.pgpainless.decryption_verification.OpenPgpMetadata;
import org.pgpainless.decryption_verification.SignatureVerification;
import org.pgpainless.exception.MalformedOpenPgpMessageException;
import org.pgpainless.exception.MissingDecryptionMethodException;
import org.pgpainless.exception.WrongPassphraseException;
import org.pgpainless.util.Passphrase;
@ -135,7 +136,7 @@ public class DecryptImpl implements Decrypt {
throw new SOPGPException.CannotDecrypt();
} catch (WrongPassphraseException e) {
throw new SOPGPException.KeyIsProtected();
} catch (PGPException | IOException e) {
} catch (MalformedOpenPgpMessageException | PGPException | IOException e) {
throw new SOPGPException.BadData(e);
} finally {
// Forget passphrases after decryption

View file

@ -18,6 +18,7 @@ import org.pgpainless.decryption_verification.ConsumerOptions;
import org.pgpainless.decryption_verification.DecryptionStream;
import org.pgpainless.decryption_verification.OpenPgpMetadata;
import org.pgpainless.decryption_verification.SignatureVerification;
import org.pgpainless.exception.MalformedOpenPgpMessageException;
import sop.Verification;
import sop.exception.SOPGPException;
import sop.operation.DetachedVerify;
@ -85,7 +86,7 @@ public class DetachedVerifyImpl implements DetachedVerify {
}
return verificationList;
} catch (PGPException e) {
} catch (MalformedOpenPgpMessageException | PGPException e) {
throw new SOPGPException.BadData(e);
}
}

View file

@ -76,7 +76,7 @@ public class InlineSignImpl implements InlineSign {
}
@Override
public Ready data(InputStream data) throws SOPGPException.KeyIsProtected, IOException, SOPGPException.ExpectedText {
public Ready data(InputStream data) throws SOPGPException.KeyIsProtected, SOPGPException.ExpectedText {
for (PGPSecretKeyRing key : signingKeys) {
try {
if (mode == InlineSignAs.CleartextSigned) {

View file

@ -19,6 +19,7 @@ import org.pgpainless.decryption_verification.ConsumerOptions;
import org.pgpainless.decryption_verification.DecryptionStream;
import org.pgpainless.decryption_verification.OpenPgpMetadata;
import org.pgpainless.decryption_verification.SignatureVerification;
import org.pgpainless.exception.MalformedOpenPgpMessageException;
import sop.ReadyWithResult;
import sop.Verification;
import sop.exception.SOPGPException;
@ -53,7 +54,7 @@ public class InlineVerifyImpl implements InlineVerify {
}
@Override
public ReadyWithResult<List<Verification>> data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData {
public ReadyWithResult<List<Verification>> data(InputStream data) throws SOPGPException.NoSignature, SOPGPException.BadData {
return new ReadyWithResult<List<Verification>>() {
@Override
public List<Verification> writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature {
@ -84,7 +85,7 @@ public class InlineVerifyImpl implements InlineVerify {
}
return verificationList;
} catch (PGPException e) {
} catch (MalformedOpenPgpMessageException | PGPException e) {
throw new SOPGPException.BadData(e);
}
}

View file

@ -4,8 +4,8 @@
allprojects {
ext {
shortVersion = '1.3.9'
isSnapshot = true
shortVersion = '1.4.0-rc1'
isSnapshot = false
pgpainlessMinAndroidSdk = 10
javaSourceCompatibility = 1.8
bouncyCastleVersion = '1.72'