Skip to content

Commit f42a2e8

Browse files
committed
Merge branch 'sessionKeyDecryption' of https://github.com/pgpainless/bc-java into pgpainless-sessionKeyDecryption
2 parents f4ee6b4 + fa70560 commit f42a2e8

File tree

4 files changed

+217
-0
lines changed

4 files changed

+217
-0
lines changed

pg/src/main/java/org/bouncycastle/openpgp/PGPEncryptedDataList.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.bouncycastle.bcpg.Packet;
1515
import org.bouncycastle.bcpg.PacketTags;
1616
import org.bouncycastle.bcpg.PublicKeyEncSessionPacket;
17+
import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket;
1718
import org.bouncycastle.bcpg.SymmetricKeyEncSessionPacket;
1819
import org.bouncycastle.bcpg.UnsupportedPacketVersionException;
1920
import org.bouncycastle.util.Iterable;
@@ -128,6 +129,30 @@ public PGPEncryptedDataList(
128129
}
129130
}
130131

132+
/**
133+
* Add a decryption method using a {@link PGPSessionKey}.
134+
* This method can be used to decrypt messages which do not contain a SKESK or PKESK packet using a
135+
* session key.
136+
*
137+
* @param sessionKey session key for message decryption
138+
* @return session key encrypted data
139+
*/
140+
public PGPSessionKeyEncryptedData addSessionKeyDecryptionMethod(PGPSessionKey sessionKey)
141+
{
142+
PGPSessionKeyEncryptedData sessionKeyEncryptedData = new PGPSessionKeyEncryptedData(sessionKey, data);
143+
methods.add(sessionKeyEncryptedData);
144+
return sessionKeyEncryptedData;
145+
}
146+
147+
/** Checks whether the packet is integrity protected.
148+
*
149+
* @return <code>true</code> if there is a modification detection code package associated with
150+
* this stream
151+
*/
152+
public boolean isIntegrityProtected() {
153+
return data instanceof SymmetricEncIntegrityPacket;
154+
}
155+
131156
/**
132157
* Gets the encryption method object at the specified index.
133158
*

pg/src/main/java/org/bouncycastle/openpgp/PGPObjectFactory.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,9 @@ public Object nextObject()
135135
return new PGPLiteralData(in);
136136
case PacketTags.PUBLIC_KEY_ENC_SESSION:
137137
case PacketTags.SYMMETRIC_KEY_ENC_SESSION:
138+
case PacketTags.SYMMETRIC_KEY_ENC:
139+
case PacketTags.SYM_ENC_INTEGRITY_PRO:
140+
case PacketTags.AEAD_ENC_DATA:
138141
return new PGPEncryptedDataList(in);
139142
case PacketTags.ONE_PASS_SIGNATURE:
140143
l = new ArrayList();
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package org.bouncycastle.openpgp;
2+
3+
import org.bouncycastle.bcpg.AEADEncDataPacket;
4+
import org.bouncycastle.bcpg.BCPGInputStream;
5+
import org.bouncycastle.bcpg.InputStreamPacket;
6+
import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket;
7+
import org.bouncycastle.openpgp.operator.PGPDataDecryptor;
8+
import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory;
9+
import org.bouncycastle.util.io.TeeInputStream;
10+
11+
import java.io.EOFException;
12+
import java.io.InputStream;
13+
14+
public class PGPSessionKeyEncryptedData extends PGPEncryptedData
15+
{
16+
17+
private final PGPSessionKey sessionKey;
18+
19+
PGPSessionKeyEncryptedData(PGPSessionKey sessionKey, InputStreamPacket encData)
20+
{
21+
super(encData);
22+
this.sessionKey = sessionKey;
23+
}
24+
25+
public PGPSessionKey getSessionKey()
26+
{
27+
return sessionKey;
28+
}
29+
30+
public InputStream getDataStream(
31+
SessionKeyDataDecryptorFactory dataDecryptorFactory)
32+
throws PGPException
33+
{
34+
if (encData instanceof AEADEncDataPacket)
35+
{
36+
AEADEncDataPacket aeadData = (AEADEncDataPacket) encData;
37+
38+
if (aeadData.getAlgorithm() != getAlgorithm())
39+
{
40+
throw new PGPException("session key and AEAD algorithm mismatch");
41+
}
42+
43+
PGPDataDecryptor dataDecryptor = dataDecryptorFactory.createDataDecryptor(aeadData.getAEADAlgorithm(), aeadData.getIV(), aeadData.getChunkSize(), sessionKey.getAlgorithm(), sessionKey.getKey());
44+
45+
BCPGInputStream encIn = encData.getInputStream();
46+
47+
return new BCPGInputStream(dataDecryptor.getInputStream(encIn));
48+
}
49+
else
50+
{
51+
try
52+
{
53+
boolean withIntegrityPacket = encData instanceof SymmetricEncIntegrityPacket;
54+
PGPDataDecryptor dataDecryptor = dataDecryptorFactory.createDataDecryptor(withIntegrityPacket, sessionKey.getAlgorithm(), sessionKey.getKey());
55+
56+
return getDataStream(withIntegrityPacket, dataDecryptor);
57+
} catch (PGPException e)
58+
{
59+
throw e;
60+
} catch (Exception e)
61+
{
62+
throw new PGPException("Exception creating cipher", e);
63+
}
64+
}
65+
}
66+
67+
private InputStream getDataStream(
68+
boolean withIntegrityPacket,
69+
PGPDataDecryptor dataDecryptor)
70+
throws PGPException
71+
{
72+
try
73+
{
74+
BCPGInputStream encIn = encData.getInputStream();
75+
encIn.mark(dataDecryptor.getBlockSize() + 2); // iv + 2 octets checksum
76+
77+
encStream = new BCPGInputStream(dataDecryptor.getInputStream(encIn));
78+
79+
if (withIntegrityPacket)
80+
{
81+
truncStream = new TruncatedStream(encStream);
82+
83+
integrityCalculator = dataDecryptor.getIntegrityCalculator();
84+
85+
encStream = new TeeInputStream(truncStream, integrityCalculator.getOutputStream());
86+
}
87+
88+
byte[] iv = new byte[dataDecryptor.getBlockSize()];
89+
for (int i = 0; i != iv.length; i++)
90+
{
91+
int ch = encStream.read();
92+
93+
if (ch < 0)
94+
{
95+
throw new EOFException("unexpected end of stream.");
96+
}
97+
98+
iv[i] = (byte)ch;
99+
}
100+
101+
int v1 = encStream.read();
102+
int v2 = encStream.read();
103+
104+
if (v1 < 0 || v2 < 0)
105+
{
106+
throw new EOFException("unexpected end of stream.");
107+
}
108+
109+
110+
// Note: the oracle attack on "quick check" bytes is not deemed
111+
// a security risk for PBE (see PGPPublicKeyEncryptedData)
112+
113+
boolean repeatCheckPassed = iv[iv.length - 2] == (byte)v1
114+
&& iv[iv.length - 1] == (byte)v2;
115+
116+
// Note: some versions of PGP appear to produce 0 for the extra
117+
// bytes rather than repeating the two previous bytes
118+
boolean zeroesCheckPassed = v1 == 0 && v2 == 0;
119+
120+
if (!repeatCheckPassed && !zeroesCheckPassed)
121+
{
122+
encIn.reset();
123+
throw new PGPDataValidationException("data check failed.");
124+
}
125+
126+
return encStream;
127+
}
128+
catch (PGPException e)
129+
{
130+
throw e;
131+
}
132+
catch (Exception e)
133+
{
134+
throw new PGPException("Exception creating cipher", e);
135+
}
136+
}
137+
138+
@Override
139+
public int getAlgorithm()
140+
{
141+
return sessionKey.getAlgorithm();
142+
}
143+
144+
}

pg/src/test/java/org/bouncycastle/openpgp/test/PGPSessionKeyTest.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.bouncycastle.openpgp.PGPSecretKey;
2222
import org.bouncycastle.openpgp.PGPSecretKeyRing;
2323
import org.bouncycastle.openpgp.PGPSessionKey;
24+
import org.bouncycastle.openpgp.PGPSessionKeyEncryptedData;
2425
import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
2526
import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory;
2627
import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
@@ -110,6 +111,8 @@ public void performTest()
110111
verifyJcePBEDecryptorFactoryFromSessionKeyCanDecryptDataSuccessfully();
111112

112113
testSessionKeyFromString();
114+
115+
decryptMessageWithoutEskUsingSessionKey();
113116
}
114117

115118
private void verifyPublicKeyDecryptionYieldsCorrectSessionData()
@@ -264,4 +267,46 @@ private void testSessionKeyFromString()
264267
isEquals("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD", Hex.toHexString(sessionKey.getKey()).toUpperCase());
265268
//isEquals(sessionKeyString, sessionKey.toString());
266269
}
270+
271+
// Message consisting only of a symmetrically encrypted integrity protected data packet wrapping a literal data packet
272+
// see https://dump.sequoia-pgp.org/?data=-----BEGIN%20PGP%20MESSAGE-----%0D%0AVersion%3A%20PGPainless%0D%0A%0D%0A0j8BNtJwO2PLoRdG%2BVynivV7XpHp2Nw/S489vksUKct67CYTFpVTzB4IcJwmUGMm%0D%0Are/N1KMTznEBzy3Txa1QVBc%3D%0D%0A%3Dt%2Bpk%0D%0A-----END%20PGP%20MESSAGE-----&session_key=26BE99BC478520FBC8AB8FB84991DACE4B82CFB9B00F7D05C051D69B8CEA8A7F
273+
private static final String encryptedMessageWithoutESK = "" +
274+
"-----BEGIN PGP MESSAGE-----\n" +
275+
"Version: PGPainless\n" +
276+
"\n" +
277+
"0j8BNtJwO2PLoRdG+VynivV7XpHp2Nw/S489vksUKct67CYTFpVTzB4IcJwmUGMm\n" +
278+
"re/N1KMTznEBzy3Txa1QVBc=\n" +
279+
"=t+pk\n" +
280+
"-----END PGP MESSAGE-----";
281+
282+
private static final PGPSessionKey sessionKey = PGPSessionKey.fromAsciiRepresentation(
283+
"9:26be99bc478520fbc8ab8fb84991dace4b82cfb9b00f7d05c051d69b8cea8a7f");
284+
285+
private void decryptMessageWithoutEskUsingSessionKey()
286+
throws IOException, PGPException
287+
{
288+
ByteArrayInputStream msgIn = new ByteArrayInputStream(Strings.toByteArray(encryptedMessageWithoutESK));
289+
ArmoredInputStream armorIn = new ArmoredInputStream(msgIn);
290+
PGPObjectFactory objectFactory = new BcPGPObjectFactory(armorIn);
291+
PGPEncryptedDataList encryptedData = (PGPEncryptedDataList) objectFactory.nextObject();
292+
isEquals(0, encryptedData.size()); // there is no encrypted session key packet
293+
294+
// Add decryption method using a session key
295+
PGPSessionKeyEncryptedData sessionKeyEncData = encryptedData.addSessionKeyDecryptionMethod(sessionKey);
296+
isEquals(1, encryptedData.size());
297+
298+
SessionKeyDataDecryptorFactory decryptorFactory = new BcSessionKeyDataDecryptorFactory(sessionKey);
299+
InputStream decrypted = sessionKeyEncData.getDataStream(decryptorFactory);
300+
301+
objectFactory = new BcPGPObjectFactory(decrypted);
302+
PGPLiteralData literalData = (PGPLiteralData) objectFactory.nextObject();
303+
304+
ByteArrayOutputStream out = new ByteArrayOutputStream();
305+
Streams.pipeAll(literalData.getDataStream(), out);
306+
307+
literalData.getDataStream().close();
308+
decrypted.close();
309+
armorIn.close();
310+
isTrue(Arrays.equals(Strings.toByteArray("Hello, World!\n"), out.toByteArray()));
311+
}
267312
}

0 commit comments

Comments
 (0)