• 摘要
  • 1.POI读取加密Excel功能
  • 2.解决办法
  • 2.1安装JCE Unlimited
  • 2.1.1 方案
  • 2.1.2 结果
  • 2.1.3 原因
  • 2.2.Reflection
  • 2.2.1 方案
  • 2.2.2 结果
  • 2.2.3 注意
  • 参考文献


摘要

解决POI读取加密Excel文件时报的 EncryptedDocumentException: Export Restrictions in place错误,尝试了JCE和反射两种方法,猜想了一下JCE失败的原因。最后发现JCE是必须安装的,只是未必会生效,有条件。具体条件没有深究。

1.POI读取加密Excel功能

随便在网上搜一搜都会有这样功能的代码,贴在下面了。

public static XSSFWorkbook readExcel(String filePath, String password)
            throws IOException, GeneralSecurityException {
        File excelFile = new File(filePath);
        InputStream is = new FileInputStream(excelFile);
        XSSFWorkbook xssfWorkbook = null;

        POIFSFileSystem poifsFileSystem = new POIFSFileSystem(is);
        is.close();
        EncryptionInfo encryptionInfo = new EncryptionInfo(poifsFileSystem);
        Decryptor decryptor = Decryptor.getInstance(encryptionInfo);
        boolean verifyPassword = decryptor.verifyPassword(password);
        if (!verifyPassword) {
            throw new SystemException("excel 密码错误!");
        }
        xssfWorkbook = new XSSFWorkbook(decryptor.getDataStream(poifsFileSystem));
        /*
         * try { xssfWorkbook=(XSSFWorkbook) WorkbookFactory.create(excelFile,password);
         * } catch (EncryptedDocumentException e) { 
         * e.printStackTrace(); } catch (InvalidFormatException e) { 
         * Auto-generated catch block e.printStackTrace(); }
         */

        return xssfWorkbook;
    }

注释部分是POI自带的工具类,功能实现和方法里面一样。但是使用这段代码读取加密文件的时候同样会报错:

org.apache.poi.EncryptedDocumentException: Export Restrictions in place - please install JCE Unlimited Strength Jurisdiction Policy files
    at org.apache.poi.poifs.crypt.CryptoFunctions.getCipher(CryptoFunctions.java:226)
    at org.apache.poi.poifs.crypt.CryptoFunctions.getCipher(CryptoFunctions.java:200)
    at org.apache.poi.poifs.crypt.agile.AgileDecryptor.hashInput(AgileDecryptor.java:269)
    at org.apache.poi.poifs.crypt.agile.AgileDecryptor.verifyPassword(AgileDecryptor.java:114)

实际上是因为excel2013用的是256bit的SHA512去加密,但是java的Ciper功能会因为不同地区的要求受到不同的限制,恰巧有限制。所以只需要把限制放开就行了。

2.解决办法

2.1安装JCE Unlimited

2.1.1 方案

按照堆栈错误提示,很容易在网上找到解决步骤。首先下载安装的文件(oracle jdk8,oracle jdk 7),根据提示,放到使用的jdk home目录下的jre/lib/security目录下。这样再执行上面的方法就行了。

2.1.2 结果

很失望,我测试的结果是不行的。我有两个版本的jdk在电脑上,运行的是1.8,项目使用的是1.7。安装完成之后已经可以在项目中发现这个两个jar包了,但是仍然报同样的错误!

POIFSFileSystem 如何加密_jar

\img\jce_jar_in_project.png)

2.1.3 原因

打开local_policy.jar文件,可以看到其中有两个文件:default_local.policy和exempt_local.policy。我的两个文件内容如下:

default_local.policy

// Some countries have import limits on crypto strength. This policy file
// is worldwide importable.

grant {
    permission javax.crypto.CryptoPermission "DES", 64;
    permission javax.crypto.CryptoPermission "DESede", *;
    permission javax.crypto.CryptoPermission "RC2", 128, 
                                     "javax.crypto.spec.RC2ParameterSpec", 128;
    permission javax.crypto.CryptoPermission "RC4", 128;
    permission javax.crypto.CryptoPermission "RC5", 128, 
          "javax.crypto.spec.RC5ParameterSpec", *, 12, *;
    permission javax.crypto.CryptoPermission "RSA", *;
    permission javax.crypto.CryptoPermission *, 128;
};

exempt_local.policy

// Some countries have import limits on crypto strength. So this file
// will be useful.

grant {
    // There is no restriction to any algorithms if KeyRecovery is enforced.
    permission javax.crypto.CryptoPermission *, "KeyRecovery"; 

    // There is no restriction to any algorithms if KeyEscrow is enforced.
    permission javax.crypto.CryptoPermission *, "KeyEscrow"; 

    // There is no restriction to any algorithms if KeyWeakening is enforced. 
    permission javax.crypto.CryptoPermission *, "KeyWeakening";
};

另外还有一个文件:US_export_policy.jar,按注释的说明没有对加密有限制和要求了。内容如下:

US_export_policy.jar

// Manufacturing policy file.
grant {
    // There is no restriction to any algorithms.
    permission javax.crypto.CryptoAllPermission; 
};

失败原因猜想:jdk是加载default_local.policy的策略,但是文件中并没有把256bite的方法加入permission。网上其他人说可以下载解决的,估计是default_local.policy有对应他们需要的加密算法。

2.2.Reflection

2.2.1 方案

从方案2.1中可以发现是因为jdk中的配置文件导致jdk对加密算法的秘钥长度有要求,如果能通过其他方法改掉这个限制就行了。实际上最后这个限制是用一个boolean字段标识,可以通过反射在解密前去除这个限制就行了。

在之前的工具类中增加下面的代码,在调用使用反射获取到标识字段,修改为false就行了。

static {
        removeCryptographyRestrictions();
    }

    /**
     * 去掉加密秘钥长度的限制
     */
    private static void removeCryptographyRestrictions() {
        if (!isRestrictedCryptography()) {
            return;
        }
        try {
            /*
             * Do the following, but with reflection to bypass access checks:
             * 
             * JceSecurity.isRestricted = false; JceSecurity.defaultPolicy.perms.clear();
             * JceSecurity.defaultPolicy.add(CryptoAllPermission.INSTANCE);
             */
            final Class<?> jceSecurity = Class.forName("javax.crypto.JceSecurity");
            final Class<?> cryptoPermissions = Class.forName("javax.crypto.CryptoPermissions");
            final Class<?> cryptoAllPermission = Class.forName("javax.crypto.CryptoAllPermission");

            Field isRestrictedField = jceSecurity.getDeclaredField("isRestricted");
            isRestrictedField.setAccessible(true);
            setFinalStatic(isRestrictedField, true);
            isRestrictedField.set(null, false);

            final Field defaultPolicyField = jceSecurity.getDeclaredField("defaultPolicy");
            defaultPolicyField.setAccessible(true);
            final PermissionCollection defaultPolicy = (PermissionCollection) defaultPolicyField.get(null);

            final Field perms = cryptoPermissions.getDeclaredField("perms");
            perms.setAccessible(true);
            ((Map<?, ?>) perms.get(defaultPolicy)).clear();

            final Field instance = cryptoAllPermission.getDeclaredField("INSTANCE");
            instance.setAccessible(true);
            defaultPolicy.add((Permission) instance.get(null));
        }
        catch (final Exception e) {
            log.error(e.getMessage(),e);
        }
    }

    static void setFinalStatic(Field field, Object newValue) throws Exception {
        field.setAccessible(true);

        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

        field.set(null, newValue);
    }

    /**
     * 判断环境是否需要去除加密限制
     * @return
     */
    private static boolean isRestrictedCryptography() {
     // This matches Oracle Java 7 and 8, but not Java 9 or OpenJDK.
        final String name = System.getProperty("java.runtime.name");
        final String ver = System.getProperty("java.version");
        return name != null && name.equals("Java(TM) SE Runtime Environment")
                && ver != null && (ver.startsWith("1.7") || ver.startsWith("1.8"));
    }

2.2.2 结果

运行解密方法,成功读取文件!

2.2.3 注意

即使是使用2.2方案的这段代码,依然需要安装JCE


参考文献

[1] how-to-avoid-installing-unlimited-strength-jce-policy-files-when-deploying-an

[2] java-opening-password-protected-xslx-file-throws-encrypteddocumentexception

[3] java-error-please-install-jce-unlimited-strength-jurisdiction-policy-files