0xTrustTryEP

Just do it, deeply...

Follow me on GitHub

JCE加密机制的一次填坑

write by donaldhan, 2020-08-07 17:42

引言

为了节省成本,需要将测试环境迁移到免费的云主机上。在项目应用验证的过程中,遇到一个问题。具体为项目中用到一个AES256加密算法加密一些数据库字段,每次在做应用迁移和应用的部署的问题相关的加密机制总会引起这样,那样的问题。今天记录一下填坑的过程;

目录

填坑

下面我们演绎一下整个过程填坑的过程。

首先应用部署完,启动日志没有任何事情,但测试应用的时候出现相关的加密工具类初始化失败问题。根据过去的经验和看代码, 怀疑可能是在加载秘钥文件的时候出错, 应在加密工具类,所有的服务委托给第三方加密包中的一个加密服务CyperService,CyperService初始话的时候,会加载响应的秘钥文件;查看用户home和应用资源目录存在对应的秘钥文件,排除这个问题,同时在加载秘钥文件地方 重写加密服务CyperService添加相关日志,重新启动,发现如下日志:

failed enable JceSecurity, install UnlimitedJCEPolicyJDK8 instead。

立马google;

搜出一大堆关于UnlimitedJCEPolicyJDK8的问题, 大部分是关于无限制加密策略的配置;

相关文章如下:

主要配置的{JRE_HOME}/lib/security/java.security文件中的加密配置项如下:

crypto.policy=unlimited

修改应用服务器上的JDK相关配置和打包编译应用Jenkins相关的JDK相关无策略加密配置, 重新部署应用,还是出现相关错误信息。

再次查看代码CyperService集成一个抽象加密服务类AbstractCyperService,加密服务类有一段静态语句块如下:

static{
    try {
        Field field = Class.forName("javax.crypto.JceSecurity").getDeclaredField("isRestricted");
        field.setAccessible(true);
        field.set((Object)null, Boolean.FALSE);
    } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException | ClassNotFoundException var1) {
        throw new Error("failed enable JceSecurity, install UnlimitedJCEPolicyJDK8 instead");
    }
}

是JceSecurity类没有加载的问题吗, MD赶快复习一下类加载机制( 类双亲加载模型)、class字节码相关的知识点;

使用如下命令查看虚拟机加载的类

java -verbose

现将JVM加载的类输出到文件,在查找,发现没有发现相应的类JceSecurity

java -verbose >> javaclass.txt
cat javaclass.txt  | grep jce

查看jre的lib文件下JceSecurity的宿主包jce.jar ,发现应用用户valuewithtime没有权限;

total 248
drwxr-xr-x  3 uucp  143   4096 Aug  6 02:01 .
drwxr-xr-x 15 uucp  143   4096 Mar 29  2018 ..
-rw-r--r--  1 uucp  143   4054 Mar 29  2018 blacklist
-rw-r--r--  1 uucp  143   1273 Mar 29  2018 blacklisted.certs
-rw-r--r--  1 uucp  143 114757 Mar 29  2018 cacerts
-rw-r--r--  1 uucp  143   2466 Mar 29  2018 java.policy
-rw-r--r--  1 uucp  143  41529 Aug  6 01:34 java.security
-rw-r--r--  1 root root  41530 Aug  6 01:32 java.security.bak
-rw-r--r--  1 uucp  143     98 Mar 29  2018 javaws.policy
-rw-r--r--  1 root root   5372 Aug  6 02:01 local_policy.jar
drwxr-xr-x  4 uucp  143   4096 Mar 29  2018 policy
-rw-r--r--  1 uucp  143      0 Mar 29  2018 trusted.libraries
-rw-r--r--  1 root root   5373 Aug  6 02:01 US_export_policy.jar


drwxr-xr-x  4 uucp  143      4096 Aug  6 02:45 .
drwxr-xr-x 11 root root      4096 Jul 27 03:19 ..
-rw-r--r--  1 root root      5307 Mar 20  2012 jce_policy-8.zip
drwxr-xr-x  8 uucp  143      4096 Mar 29  2018 jdk1.8.0_171
drwxr-xr-x  7 uucp  143      4096 Oct  5  2019 jdk1.8.0_231
-rw-r--r--  1 root root 194151339 Aug  6 02:38 jdk-8u231-linux-x64.tar.gz
-rw-r--r--  1 root root      5372 Jun 19 21:09 local_policy.jar
-rw-r--r--  1 root root      5373 Jun 19 21:09 US_export_policy.jar

修改响应文件目录的权限

chown -R  valuewithtime:valuewithtime ./*

重新编译打包部署,仍没有解决问题。

MD要,赶快把重新抽象加密的服务静态语句块的异常堆栈打印出来:

 static {
        try {
            Field field = Class.forName("javax.crypto.JceSecurity").getDeclaredField("isRestricted");
            field.setAccessible(true);
            field.set((Object)null, Boolean.FALSE);
        } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException | ClassNotFoundException var1) {
            log.error("enable JceSecurity fail",var1);
        }
    ...
    }

重新编译打包部署,,抛出如下错误

[2020-08-06 08:07:10] [localhost-startStop-1] ERROR c.n.l.c.j
java.lang.IllegalAccessException: Can not set static final boolean field javax.crypto.JceSecurity.isRestricted to java.lang.Boolean
        at sun.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:76)
        at sun.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:80)
        at sun.reflect.UnsafeQualifiedStaticBooleanFieldAccessorImpl.set(UnsafeQualifiedStaticBooleanFieldAccessorImpl.java:77)
        at java.lang.reflect.Field.set(Field.java:764)

从异常信息来看,不能重写JceSecurity的final静态isRestricted属性; 查看项目工程中JceSecurity中的字段发现isRestricted为静态,应该可以修改的:

private static boolean isRestricted = true;

再次google,搜索出好多通过反射,INT类转换,设置isRestricted属性文章,感觉不靠谱;

查看老测试环境的jdk为jdk1.8.0_171, 现在测试环境也为jdk1.8.0_171,本地项目为jdk1.8.0_101,查看线上的jdk版本也为jdk1.8.0_101;

查看相应的jdk1.8.0_171和jdk1.8.0_101版本JceSecurity的isRestricted定义具体如下:

jdk1.8.0_171

final class JceSecurity {
private static final Map<Provider, Object> verificationResults = new IdentityHashMap();
private static final Map<Provider, Object> verifyingProviders = new IdentityHashMap();
private static final boolean isRestricted;
private static final Debug debug = Debug.getInstance("jca", "Cipher");
private static final Object PROVIDER_VERIFIED;
private static final URL NULL_URL;
private static final Map<Class<?>, URL> codeBaseCacheRef = new WeakHashMap();
...
}

jdk1.8.0_101

final class JceSecurity {
static final SecureRandom RANDOM = new SecureRandom();
private static CryptoPermissions defaultPolicy = null;
private static CryptoPermissions exemptPolicy = null;
private static final Map<Provider, Object> verificationResults = new IdentityHashMap();
private static final Map<Provider, Object> verifyingProviders = new IdentityHashMap();
private static boolean isRestricted = true;
private static final Object PROVIDER_VERIFIED;
private static final URL NULL_URL;
private static final Map<Class<?>, URL> codeBaseCacheRef;
...
}

对比代码发现jdk1.8.0_101的isRestricted为static,而jdk1.8.0_171为final static, 由于jdk1.8.0_171的为final static导致 修改final属性出错。

修改测试环境上的jdk为jdk1.8.0_101, 发现一切OK, BINGO。

Google JceSecurity的信息,发现isRestricted从1.8.0_102开始从static变为final static。

没文化真可拍,一句卧槽走天下。

回过头来,复盘整个事情,首先第三方JAR中对异常信息的重新,并将异常堆栈吃掉,导致我们走了不少的弯路。 其中最主要的问题还是,没有这个流程搞清楚,一心想直接解决问题。

再次从零开始查看这个加密机制, 什么地方用到了JceSecurity的isRestricted。

查看加密类Cipher, 发现在获取Cipher实例的时候,initCryptoPermission会用到。 //Cipher

  public static final Cipher getInstance(String var0, Provider var1) throws NoSuchAlgorithmException, NoSuchPaddingException {
        if (var1 == null) {
            throw new IllegalArgumentException("Missing provider");
        } else {
            Exception var2 = null;
            List var3 = getTransforms(var0);
            boolean var4 = false;
            String var5 = null;
            Iterator var6 = var3.iterator();

            while(true) {
                while(true) {
                    Cipher.Transform var7;
                    Service var8;
                    do {
                        do {
                            if (!var6.hasNext()) {
                                if (var2 instanceof NoSuchPaddingException) {
                                    throw (NoSuchPaddingException)var2;
                                }

                                if (var5 != null) {
                                    throw new NoSuchPaddingException("Padding not supported: " + var5);
                                }

                                throw new NoSuchAlgorithmException("No such algorithm: " + var0, var2);
                            }

                            var7 = (Cipher.Transform)var6.next();
                            var8 = var1.getService("Cipher", var7.transform);
                        } while(var8 == null);

                        if (!var4) {
                            Exception var9 = JceSecurity.getVerificationResult(var1);
                            if (var9 != null) {
                                String var12 = "JCE cannot authenticate the provider " + var1.getName();
                                throw new SecurityException(var12, var9);
                            }

                            var4 = true;
                        }
                    } while(var7.supportsMode(var8) == 0);

                    if (var7.supportsPadding(var8) != 0) {
                        try {
                            CipherSpi var13 = (CipherSpi)var8.newInstance((Object)null);
                            var7.setModePadding(var13);
                            Cipher var10 = new Cipher(var13, var0);
                            var10.provider = var8.getProvider();
                            //初始化加密权限策略
                            var10.initCryptoPermission();
                            return var10;
                        } catch (Exception var11) {
                            var2 = var11;
                        }
                    } else {
                        var5 = var7.pad;
                    }
                }
            }
        }
    }

//初始化加密许可策略

private void initCryptoPermission() throws NoSuchAlgorithmException {
    if (!JceSecurity.isRestricted()) {
        //非严格限制下,为CryptoAllPermission
        this.cryptoPerm = CryptoAllPermission.INSTANCE;
        this.exmech = null;
    } else {
        //否则从JDK的jre的lig/security目录下,加载相关安全权限
        this.cryptoPerm = getConfiguredPermission(this.transformation);
        String var1 = this.cryptoPerm.getExemptionMechanism();
        if (var1 != null) {
            this.exmech = ExemptionMechanism.getInstance(var1);
        }
    }
}

Cipher初始化时,如果JceSecurity为非严格模式(isRestricted=false),则为CryptoAllPermission。 否则从JDK的jre的lig/security目录下,加载相关安全权限。

为什么加密的时候,要重新JceSecurity的isRestricted为false,具体原因为jdk1.8.0_101之前, Cipher相关的AES加密算法不支持256加密方式 ,只能通过重新JceSecurity的isRestricted为false, 剔除限制。

第三方jar的异常包装信息的含义是什么呢?

failed enable JceSecurity, install UnlimitedJCEPolicyJDK8 instead

从错误来看,应该是需要安装无限制安全策略的JDK8版本。

查看{JRE_HOME}/lib/security/java.security文件中关于crypto.policy的属性描述:

# Cryptographic Jurisdiction Policy defaults
#
# Import and export control rules on cryptographic software vary from
# country to country.  By default, the JDK provides two different sets of
# cryptographic policy files:
#
#     unlimited:  These policy files contain no restrictions on cryptographic
#                 strengths or algorithms.
#
#     limited:    These policy files contain more restricted cryptographic
#                 strengths, and are still available if your country or
#                 usage requires the traditional restrictive policy.
#
# The JDK JCE framework uses the unlimited policy files by default.
# However the user may explicitly choose a set either by defining the
# "crypto.policy" Security property or by installing valid JCE policy
# jar files into the traditional JDK installation location.  To better
# support older JDK Update releases, the "crypto.policy" property is not
# defined by default.  See below for more information.
#
# The following logic determines which policy files are used:
#
#         <java-home> refers to the directory where the JRE was
#         installed and may be determined using the "java.home"
#         System property.
#
# 1.  If the Security property "crypto.policy" has been defined,
#     then the following mechanism is used:
#  如果定义了crypto.policy,将使用如下机制策略:
#     The policy files are stored as jar files in subdirectories of
# <java-home>/lib/security/policy.  Each directory contains a complete
# set of policy files.
# 策略相关的额文件存储在<java-home>/lib/security/policy目录下。每个目录包含一个完成的
策略文件。
#     The "crypto.policy" Security property controls the directory
#     selection, and thus the effective cryptographic policy.
#   加密策略控制如下:
# The default set of directories is:
# 两种模式:
#     limited | unlimited
#
# 2.  If the "crypto.policy" property is not set and the traditional
#     US_export_policy.jar and local_policy.jar files
#     (e.g. limited/unlimited) are found in the legacy
#     <java-home>/lib/security directory, then the rules embedded within
#     those jar files will be used. This helps preserve compatibility
# for users upgrading from an older installation.
#  如果crypto.policy属性没有设置,在<java-home>/lib/security目下找到传统的
US_export_policy.jar and local_policy.jar文件,则jar包内相关的配置文件将会使用
# 3.  If the jar files are not present in the legacy location
#     and the "crypto.policy" Security property is not defined,
#     then the JDK will use the unlimited settings (equivalent to
#     crypto.policy=unlimited)
# 如果在规定的文件位置jar文件不存在,且crypto.policy没有定义,则默认使用unlimited;
# Please see the JCA documentation for additional information on these
# files and formats.
#
# YOU ARE ADVISED TO CONSULT YOUR EXPORT/IMPORT CONTROL COUNSEL OR ATTORNEY
# TO DETERMINE THE EXACT REQUIREMENTS.
#
# Please note that the JCE for Java SE, including the JCE framework,
# cryptographic policy files, and standard JCE providers provided with
# the Java SE, have been reviewed and approved for export as mass market
# encryption item by the US Bureau of Industry and Security.
#
# Note: This property is currently used by the JDK Reference implementation.
# It is not guaranteed to be examined and used by other implementations.
#
#crypto.policy=unlimited

从上面可以看出,JCE策略默认情况下,就是无限制策略,不需要变动;

另外从1.8.0_151后,在/lib/security目录下相关安全策略jar包 US_export_policy.jar 和 local_policy.jar文件已经移除,新的安全策略相关的文件,存储在/lib/security/policy目录下。

从上面来看,包装的异常信息完全错误。

总结

从这次填坑的过程,我们学到一下两个知识点:

  • jdk1.8.0_101之前, Cipher相关的AES加密算法不支持256加密方式,只能通过重新JceSecurity的isRestricted为false, 剔除限制。

  • 从1.8.0_151后,在/lib/security目录下相关安全策略jar包 US_export_policy.jar 和 local_policy.jar文件已经移除,新的安全策略相关的文件,存储在/lib/security/policy目录下。

得到的经验教训:

  • 全面了解问题,才入手解决;
  • 不要包装异常;
  • 针对异常,需要打印异常堆栈。

ARTICLES JCE unlimited cipher policy with different JDK versions

Java Unlimited Strength Crypto Policy for Java 9 or 1.8.0_151

JDK1.8.0_151的无限制强度加密策略文件变动
Java部署应用程序时如何避免安装“ Unlimited Strength” JCE策略文件?

类加载机制

深入理解JVM之Java字节码(.class)文件详解 java-verbose使用

JDK 8u102 changed JceSecurity#isRestricted to final #4101
Use final restricted flag

How to avoid installing “Unlimited Strength” JCE policy files when deploying an application?
Java-安全模型
JDK安全模块JCE核心Cipher使用详解
Java AES 256 Encryption Decryption Example
Galois/Counter Mode
AES-GCM 加密简介