Node.js中Google Integrity的JWE解密失败原因求助
Play Integrity API JWE令牌解密失败排查与解决方案
我正在用Node.js/Express编写反向代理以提升API调用安全性,根据文档编写了解密Google Play Integrity API返回的JWE令牌的代码,但即使使用Google Play Console提供的正确DECRYPTION_KEY,解密仍失败。测试用的JWE令牌来自通过Expo Metro运行的应用(而非Play商店正式安装的应用),以下是相关代码:
app.get('/checkGoogleToken', async (req, res) => { const tokens = fs.readdirSync('./tokens/') console.log('Checking Google Token') const token = fs.readFileSync('./tokens/' + tokens[0], 'utf-8') console.log(token) const { plaintext, protectedHeader } = await jose.compactDecrypt( token, Buffer.from(process.env.DECRYPTION_KEY, "base64") ); /* integrity_token is the token got from Integrity API response in the app. DECRYPTION_KEY is found from Google Play Console */ console.log(protectedHeader); console.log(new TextDecoder().decode(plaintext)); const { payload, Header = protectedHeader } = await jose.compactVerify( plaintext, crypto.createPublicKey("-----BEGIN PUBLIC KEY-----\n" + process.env.VERIFICATION_KEY + "\n-----END PUBLIC KEY-----") ) })
核心结论:Expo Metro运行环境确实会导致密钥不匹配
是的,通过Expo Metro直接运行应用而非从Play商店安装,必然会导致Play Integrity API生成的JWE令牌使用不同的密钥体系,原因如下:
- Play Integrity API的令牌加密逻辑与应用的签名身份强绑定:只有当应用通过Google Play官方签名渠道发布(包括上传App Bundle后由Google签名,或使用「App Signing by Google Play」的自有密钥),Google才会使用你在Play Console配置的正式DECRYPTION_KEY加密JWE令牌。
- Expo Metro运行的应用使用的是调试签名证书(默认是Expo提供的调试密钥,或你本地生成的调试密钥),而非Play商店的正式签名密钥。此时Google会自动匹配调试签名对应的临时密钥加密令牌,而非你配置的正式密钥,自然无法用正式DECRYPTION_KEY解密。
解决步骤
获取调试环境对应的解密密钥
在Play Console的「App Integrity」>「Play Integrity API」页面,找到调试环境配置区:- 将你的调试签名证书SHA-256指纹添加到调试允许列表
- Google会生成对应调试环境的解密密钥,将其配置到你的开发环境变量中
区分环境加载密钥
在代码中根据运行环境切换密钥,避免混用:const decryptionKey = process.env.NODE_ENV === 'production' ? Buffer.from(process.env.PROD_DECRYPTION_KEY, "base64") : Buffer.from(process.env.DEV_DECRYPTION_KEY, "base64"); const { plaintext, protectedHeader } = await jose.compactDecrypt(token, decryptionKey);同步验证密钥的环境匹配
调试环境下的令牌签名公钥也需要使用对应调试环境的密钥,否则后续compactVerify步骤同样会失败,需同步配置调试环境的VERIFICATION_KEY。临时测试方案(仅用于开发,禁止生产使用)
如果需要快速验证逻辑,可临时将调试签名指纹添加到生产环境的允许列表,但测试完成后务必移除,避免引入安全风险。
代码优化建议
- 替换同步文件操作:
fs.readdirSync和fs.readFileSync会阻塞事件循环,建议改用异步版本适配Express异步路由:const tokens = await fs.promises.readdir('./tokens/'); const token = await fs.promises.readFile(`./tokens/${tokens[0]}`, 'utf-8'); - 增加异常捕获:避免因目录为空、文件不存在导致服务崩溃:
app.get('/checkGoogleToken', async (req, res) => { try { const tokens = await fs.promises.readdir('./tokens/'); if (tokens.length === 0) { return res.status(400).json({ error: 'No tokens found in directory' }); } const token = await fs.promises.readFile(`./tokens/${tokens[0]}`, 'utf-8'); // 后续解密逻辑... } catch (err) { console.error('Error processing token:', err); return res.status(500).json({ error: 'Failed to process token' }); } })
内容的提问来源于stack exchange,提问作者VictoriaStudios
相关产品推荐
相关产品推荐

