Quarkus集成测试与Docker读取密钥文件仅29字节,单元测试正常
问题现象
在Quarkus/Docker环境中读取二进制密钥文件时,仅能读取到29字节,但单元测试中可正常读取完整32字节,导致集成测试失败、本地Docker运行时抛出JWT解密异常。生产环境通过Openshift数据卷挂载密钥文件时运行正常,仅本地打包Jar后出现问题。
异常日志
Caused by: com.nimbusds.jose.JOSEException: AES/GCM/NoPadding decryption failed: Tag mismatch! at com.nimbusds.jose.crypto.impl.AESGCM.decrypt(AESGCM.java:301) at com.nimbusds.jose.crypto.impl.ContentCryptoProvider.decrypt(ContentCryptoProvider.java:279) at com.nimbusds.jose.crypto.DirectDecrypter.decrypt(DirectDecrypter.java:271) at com.nimbusds.jose.JWEObject.decrypt(JWEObject.java:415) at com.org.TokenParser.parseToken(TokenParser.java:38) ... 13 more Caused by: javax.crypto.AEADBadTagException: Tag mismatch! at java.base/com.sun.crypto.provider.GaloisCounterMode.decryptFinal(GaloisCounterMode.java:623) at java.base/com.sun.crypto.provider.CipherCore.finalNoPadding(CipherCore.java:1116) at java.base/com.sun.crypto.provider.CipherCore.fillOutputBuffer(CipherCore.java:1053) at java.base/com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:853) at java.base/com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:446) at java.base/javax.crypto.Cipher.doFinal(Cipher.java:2202) at com.nimbusds.jose.crypto.impl.AESGCM.decrypt(AESGCM.java:297)
场景与代码细节
密钥读取逻辑
代码优先尝试读取/opt/data/environment路径下的密钥文件,若不存在则 fallback 到类路径资源(src/test/resources或src/main/resources)。
JWT解密代码
import com.nimbusds.jose.JWEDecrypter; import com.nimbusds.jose.crypto.factories.DefaultJWEDecrypterFactory; import com.nimbusds.jwt.EncryptedJWT; import com.nimbusds.jwt.JWTClaimsSet; ... EncryptedJWT jwt = EncryptedJWT.parse(token); DefaultJWEDecrypterFactory factory = new DefaultJWEDecrypterFactory(); JWEDecrypter decrypter = factory.createJWEDecrypter(jwt.getHeader(), this.secretKey); jwt.decrypt(decrypter); // 此处抛出Tag mismatch异常
SecretKey创建代码
SecretKeySpec secretKey = new SecretKeySpec(data,"AES");
密钥读取方法
static byte[] readKeyFromInputStream(final InputStream sharedKeyStream) throws IOException, NoSuchAlgorithmException { byte[] keyBytes; if (sharedKeyStream == null) { keyBytes = autoGenerateKey(); } else { keyBytes = new byte[sharedKeyStream.available()]; // 可用字节为32 // 使用"read(keyBytes)"仅读取29字节,尝试显式读取32字节仍失败 int count = sharedKeyStream.read(keyBytes, 0, 32); LOG.infof("Activating security token handler with key from input stream (length = %d bytes)", count); //if (count != 32) { // throw new IOException("Key read bytes length does not match expected number(32), cannot initialize key!"); //} } return keyBytes; }
通过的单元测试
@Test void shouldRead32BytesFromInputStream() throws IOException, NoSuchAlgorithmException { // given InputStream keyFileStream = TokenHandlerTest.class.getResourceAsStream("/key.shared"); // when byte[] keyFileBytes = TokenHandler.readKeyFromInputStream(keyFileStream); // then assertEquals(32, keyFileBytes.length); }
关键观察
- 本地通过Gnome文件管理器和
wc -c命令确认密钥文件为32字节 - 将密钥文件放入
src/jib/opt/data/environment由Jib Maven插件打包进Docker镜像后,运行正常 - 替换为32字节纯文本密钥文件时,集成测试可正常读取32字节并通过
- 原密钥为二进制文件,通过
od查看十六进制内容,推测打包Jar时被压缩导致内容截断
疑问
- 原
AEADBadTagException异常是否与密钥长度不匹配直接相关? - 如何在集成测试中正确加载本地二进制密钥文件并读取完整32字节?
回答
1. 异常与密钥长度的关联
是的,该异常直接由密钥长度不匹配导致。AES-256要求密钥必须为32字节,当读取到的密钥仅29字节时,SecretKeySpec会将这29字节作为有效密钥使用,导致解密时的密钥与加密方的32字节密钥不一致,最终触发GCM模式的标签校验失败(Tag mismatch)。
2. 解决集成测试中完整读取二进制密钥的问题
问题根源是Maven打包Jar时,默认会对类路径下的文件进行过滤或压缩,二进制文件的特殊字节可能被篡改或截断。以下是三种可行解决方案:
方案1:配置Maven资源插件,排除二进制密钥文件的处理
在pom.xml中修改maven-resources-plugin配置,将密钥文件后缀标记为无需过滤的二进制文件:
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>3.3.1</version> <configuration> <nonFilteredFileExtensions> <nonFilteredFileExtension>shared</nonFilteredFileExtension> </nonFilteredFileExtensions> </configuration> </plugin> </plugins> </build>
此配置会让Maven保留.shared文件的原始二进制内容,避免打包时的篡改。
方案2:改进密钥读取逻辑,确保读取完整内容
当前读取逻辑依赖InputStream.available()和单次read()调用,这在流被压缩或缓冲时不可靠。替换为循环读取直到获取完整32字节:
static byte[] readKeyFromInputStream(final InputStream sharedKeyStream) throws IOException, NoSuchAlgorithmException { if (sharedKeyStream == null) { return autoGenerateKey(); } byte[] keyBytes = new byte[32]; int totalRead = 0; int read; while (totalRead < 32 && (read = sharedKeyStream.read(keyBytes, totalRead, 32 - totalRead)) != -1) { totalRead += read; } if (totalRead != 32) { throw new IOException("Failed to read complete 32-byte AES key, only read " + totalRead + " bytes"); } LOG.infof("Activating security token handler with key from input stream (length = %d bytes)", totalRead); return keyBytes; }
这种方式确保无论流的类型如何,都会读取到完整的32字节,避免单次读取不完整的问题。
方案3:集成测试中直接读取本地文件系统
在集成测试中绕过类路径,直接读取本地文件系统中的原始密钥文件:
@Test void shouldRead32BytesFromInputStream() throws IOException, NoSuchAlgorithmException { // 直接读取本地src/test/resources下的原始文件 Path keyPath = Paths.get("src/test/resources/key.shared"); try (InputStream keyFileStream = Files.newInputStream(keyPath)) { byte[] keyFileBytes = TokenHandler.readKeyFromInputStream(keyFileStream); assertEquals(32, keyFileBytes.length); } }
这种方式避免了Jar打包过程对文件的影响,确保测试使用的是未被修改的二进制文件。
内容的提问来源于stack exchange,提问作者WesternGun

