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