You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@poi.apache.org by fa...@apache.org on 2026/10/24 22:12:39 UTC
(poi) branch trunk updated: AgileDecryptor: verify the DataIntegrity HMAC before decrypting (#1465)
This is an automated email from the ASF dual-hosted git repository.
pjfanning pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/poi.git
The following commit(s) were added to refs/heads/trunk by this push:
new c04778ea47 AgileDecryptor: verify the DataIntegrity HMAC before decrypting (#1465)
c04778ea47 is described below
commit c04778ea477255716a25b8c31fc7eb6af6fb1ecd
Author: Nayyar <na...@bugqore.com>
AuthorDate: Sun Oct 25 03:42:33 2026 +0530
AgileDecryptor: verify the DataIntegrity HMAC before decrypting (#1465)
---
.../poi/poifs/crypt/agile/AgileDecryptor.java | 36 ++++++++++++++++++++--
.../poi/poifs/crypt/agile/TestAgileDecryptor.java | 32 +++++++++++++++++++
2 files changed, 65 insertions(+), 3 deletions(-)
diff --git a/poi/src/main/java/org/apache/poi/poifs/crypt/agile/AgileDecryptor.java b/poi/src/main/java/org/apache/poi/poifs/crypt/agile/AgileDecryptor.java
index 5aa8885958..05927b135b 100644
--- a/poi/src/main/java/org/apache/poi/poifs/crypt/agile/AgileDecryptor.java
+++ b/poi/src/main/java/org/apache/poi/poifs/crypt/agile/AgileDecryptor.java
@@ -32,6 +32,7 @@ import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays;
import javax.crypto.Cipher;
+import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.RC2ParameterSpec;
@@ -213,11 +214,43 @@ public class AgileDecryptor extends Decryptor {
@SuppressWarnings({"java:S2095"})
@Override
public InputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException {
+ verifyIntegrity(dir);
DocumentInputStream dis = dir.createDocumentInputStream(DEFAULT_POIFS_ENTRY);
_length = dis.readLong();
return new AgileCipherInputStream(dis, _length);
}
+ /**
+ * 2.3.4.14 DataIntegrity Generation (Agile Encryption)
+ *
+ * Verifies the HMAC of the entire EncryptedPackage stream, including the StreamSize field,
+ * against the decrypted encryptedHmacValue, so modifications of the encrypted data are
+ * detected before any of it is decrypted.
+ */
+ private void verifyIntegrity(DirectoryNode dir) throws IOException, GeneralSecurityException {
+ byte[] hmacKey = getIntegrityHmacKey();
+ byte[] hmacValue = getIntegrityHmacValue();
+ if (hmacKey == null || hmacValue == null) {
+ throw new EncryptedDocumentException("The password needs to be verified before the data stream can be accessed");
+ }
+
+ HashAlgorithm hashAlgo = getEncryptionInfo().getHeader().getHashAlgorithm();
+ Mac integrityMD = CryptoFunctions.getMac(hashAlgo);
+ integrityMD.init(new SecretKeySpec(hmacKey, hashAlgo.jceHmacId));
+
+ byte[] buf = new byte[4096];
+ try (DocumentInputStream dis = dir.createDocumentInputStream(DEFAULT_POIFS_ENTRY)) {
+ int readBytes;
+ while ((readBytes = dis.read(buf)) != -1) {
+ integrityMD.update(buf, 0, readBytes);
+ }
+ }
+
+ if (!MessageDigest.isEqual(hmacValue, integrityMD.doFinal())) {
+ throw new EncryptedDocumentException("The data integrity check of the encrypted package failed");
+ }
+ }
+
@Override
public long getLength(){
if(_length == -1) {
@@ -272,9 +305,6 @@ public class AgileDecryptor extends Decryptor {
super(stream, size, 4096);
}
- // TODO: calculate integrity hmac while reading the stream
- // for a post-validation of the data
-
@Override
protected Cipher initCipherForBlock(Cipher cipher, int block)
throws GeneralSecurityException {
diff --git a/poi/src/test/java/org/apache/poi/poifs/crypt/agile/TestAgileDecryptor.java b/poi/src/test/java/org/apache/poi/poifs/crypt/agile/TestAgileDecryptor.java
index de573b8138..9e4be0cc84 100644
--- a/poi/src/test/java/org/apache/poi/poifs/crypt/agile/TestAgileDecryptor.java
+++ b/poi/src/test/java/org/apache/poi/poifs/crypt/agile/TestAgileDecryptor.java
@@ -142,4 +142,36 @@ class TestAgileDecryptor {
assertThrows(EncryptedDocumentException.class, () ->
new EncryptionInfo(new LittleEndianByteArrayInputStream(modifiedEncInfoBytes), null));
}
+
+ @Test
+ void testTamperedEncryptedPackageIsRejected() throws Exception {
+ EncryptionInfo infoEnc = new EncryptionInfo(EncryptionMode.agile);
+ Encryptor enc = infoEnc.getEncryptor();
+ enc.confirmPassword("test");
+
+ byte[] encData;
+ try (POIFSFileSystem fsEnc = new POIFSFileSystem()) {
+ try (OutputStream os = enc.getDataStream(fsEnc)) {
+ os.write(new byte[3000]);
+ }
+ UnsynchronizedByteArrayOutputStream bos = UnsynchronizedByteArrayOutputStream.builder().get();
+ fsEnc.writeFilesystem(bos);
+ encData = bos.toByteArray();
+ }
+
+ try (POIFSFileSystem fs = new POIFSFileSystem(new ByteArrayInputStream(encData))) {
+ // flip one bit of the encrypted data, i.e. behind the 8 byte StreamSize field
+ byte[] encPackage;
+ try (DocumentInputStream dis = fs.getRoot().createDocumentInputStream(DEFAULT_POIFS_ENTRY)) {
+ encPackage = IOUtils.toByteArray(dis);
+ }
+ encPackage[8 + 16] ^= 0x01;
+ fs.createOrUpdateDocument(new ByteArrayInputStream(encPackage), DEFAULT_POIFS_ENTRY);
+
+ EncryptionInfo infoDec = new EncryptionInfo(fs);
+ Decryptor dec = infoDec.getDecryptor();
+ assertTrue(dec.verifyPassword("test"));
+ assertThrows(EncryptedDocumentException.class, () -> dec.getDataStream(fs));
+ }
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@poi.apache.org
For additional commands, e-mail: commits-help@poi.apache.org