使用 Apache POI 检查 Excel 宏密码

Check Excel macro password with Apache POI

能否检查Excel中的宏代码是否受密码保护,甚至验证是哪个密码?
我确实找到了有关密码保护 Excel 工作簿和受保护工作表的示例,但没有找到有关锁定宏代码的示例。

对于重新设计 Office 文档 - 您可以使用我的 POI-Visualizer - 这样您就可以轻松查看嵌入元素的结构和下落。

信息存储在 vbaProject.binPROJECT 流中。 它的MS-OVBA has an detailed example。 使用以下代码可以验证 DPB 元素 - 项目密码。

import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.openxml4j.opc.PackagePart;
import org.apache.poi.openxml4j.opc.PackagePartName;
import org.apache.poi.openxml4j.opc.PackagingURIHelper;
import org.apache.poi.poifs.crypt.CryptoFunctions;
import org.apache.poi.poifs.crypt.HashAlgorithm;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.util.LittleEndian;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Properties;

public class VBAProtect {
    private static final String NO_PASSWORD_HASH = "0E0CD1ECDFF4E7F5E7F5E7";

    public static void main(String[] args) throws DecoderException, IOException, InvalidFormatException {
        try (FileInputStream is = new FileInputStream("vba-protected.xlsm");
             XSSFWorkbook wb = new XSSFWorkbook(is)) {
            PackagePartName pn = PackagingURIHelper.createPartName("/xl/vbaProject.bin");
            PackagePart part = wb.getPackage().getPart(pn);

            try (InputStream pis = part.getInputStream();
                 POIFSFileSystem ps = new POIFSFileSystem(pis);
                 InputStream dis = ps.createDocumentInputStream("PROJECT")) {

                Properties prop = new Properties();
                prop.load(dis);
                String DPB = prop.getProperty("DPB").replace("\"", "");

                if (NO_PASSWORD_HASH.equals(DPB)) {
                    System.out.println("no password");
                } else {
                    EncData ed = new EncData();
                    ed.parse(DPB);
                    PasswordHash ph = new PasswordHash();
                    ph.parse(ed);

                    String pass = "password";
                    System.out.println("pass <" + pass + "> matches? " + ph.matches(pass));
                }
            }
        }

    }

    public static class EncData {
        public byte seed;
        public byte version;
        public byte projKey;
        public int ignoredLength;
        public int dataLength;
        private byte[] raw;

        public byte[] getData() {
            byte[] data = new byte[dataLength];
            // Header (3 bytes) + Ignored bytes + length (4 bytes)
            System.arraycopy(raw, 3 + ignoredLength + 4, data, 0, dataLength);
            return data;
        }

        public void parse(String data) throws DecoderException {
            raw = Hex.decodeHex(data);
            seed = raw[0];
            byte VersionEnc = raw[1];
            byte ProjKeyEnc = raw[2];
            ignoredLength = ((seed & 6) / 2);

            version = (byte)((seed ^ VersionEnc) & 0xFF);
            projKey = (byte)((seed ^ ProjKeyEnc) & 0xFF);
            byte UnencryptedByte1 = projKey;
            byte EncryptedByte1 = ProjKeyEnc;
            byte EncryptedByte2 = VersionEnc;

            for (int offset = 3; offset < raw.length; offset++) {
                byte ByteEnc = raw[offset];
                byte Byte = (byte)((ByteEnc ^ (EncryptedByte2 + UnencryptedByte1)) & 0xFF);
                EncryptedByte2 = EncryptedByte1;
                EncryptedByte1 = ByteEnc;
                raw[offset] = UnencryptedByte1 = Byte;
            }

            dataLength = LittleEndian.getInt(raw, 3 + ignoredLength);
        }
    }

    public static class PasswordHash {
        public final byte[] key = new byte[4];
        public final byte[] passwordHash = new byte[20];

        public void parse(EncData data) {
            byte[] dpb = data.getData();
            // first byte of grbit is reserved, so ignore it
            int Grbit = LittleEndian.getInt(dpb, 0) >>> 8;
            final int offset = 4;
            for (int i=0; i<24; i++, Grbit >>>= 1) {
                if ((Grbit & 1) == 0) {
                    dpb[offset+i] = 0;
                }
            }
            System.arraycopy(dpb, offset, key, 0, 4);
            System.arraycopy(dpb, offset+4, passwordHash, 0, 20);
        }

        public boolean matches(String password) {
            // TODO: check MBCS windows encoding differences to UTF-8
            // 
            MessageDigest shaDig = CryptoFunctions.getMessageDigest(HashAlgorithm.sha1);
            shaDig.update(password.getBytes(StandardCharsets.UTF_8));
            shaDig.update(key);
            byte[] dig = shaDig.digest();
            return Arrays.equals(dig, passwordHash);
        }
    }
}