Rails 小程序加密数据解密失败问题 (Ruby 版)

lazybios · 2017年01月15日 · 最后由 kxu1988 回复于 2021年05月19日 · 17005 次阅读

问题描述

Rails 实现的小程序后台,在做原生 APP 与小程序用户数据打通需求时,遇到了偶发性微信加密数据解密失败的情况。是的没错,解密失败的情况是偶发的!!!。报错信息如下:

OpenSSL::Cipher::CipherError: bad decrypt

就是处在了final那里。

微信给的解密算法

  • 对称解密使用的算法为 AES-128-CBC,数据采用PKCS#7填充。
  • 对称解密的目标密文为 Base64_Decode(encryptedData),
  • 对称解密秘钥 aeskey = Base64_Decode(session_key), aeskey 是 16 字节
  • 对称解密算法初始向量 iv 会在数据接口中返回。

文档地址,文档里还提供了 Node、Python、C++、Java 的示例代码,没准能用的上。

Ruby 实现的解密算法

class WXBizDataCrypt
  attr_accessor :app_id, :session_key

  def initialize(app_id, session_key)
    @app_id = app_id
    @session_key = session_key
  end

  def decrypt(encrypted_data, iv)
    session_key = Base64.decode64(@session_key)
    encrypted_data= Base64.decode64(encrypted_data)
    iv = Base64.decode64(iv)

    cipher = OpenSSL::Cipher::AES128.new(:CBC)
    cipher.decrypt
    cipher.key = session_key
    cipher.iv = iv

    decrypted = JSON.parse(cipher.update(encrypted_data) + cipher.final)
    raise('Invalid Buffer') if decrypted['watermark']['appid'] != @app_id

    decrypted
  end
end

测试代码

require './wx_biz_data_crypt'

app_id = 'wx4f4bc4dec97d474b'
iv = 'r7BXXKkLb8qrSNn05n0qiA=='
session_key = 'tiihtNczf5v6AKRyjwEUhQ=='
encrypted_data = 
  'CiyLU1Aw2KjvrjMdj8YKliAjtP4gsMZM'+
  'QmRzooG2xrDcvSnxIMXFufNstNGTyaGS'+
  '9uT5geRa0W4oTOb1WT7fJlAC+oNPdbB+'+
  '3hVbJSRgv+4lGOETKUQz6OYStslQ142d'+
  'NCuabNPGBzlooOmB231qMM85d2/fV6Ch'+
  'evvXvQP8Hkue1poOFtnEtpyxVLW1zAo6'+
  '/1Xx1COxFvrc2d7UL/lmHInNlxuacJXw'+
  'u0fjpXfz/YqYzBIBzD6WUfTIF9GRHpOn'+
  '/Hz7saL8xz+W//FRAUid1OksQaQx4CMs'+
  '8LOddcQhULW4ucetDf96JcR3g0gfRK4P'+
  'C7E/r7Z6xNrXd2UIeorGj5Ef7b1pJAYB'+
  '6Y5anaHqZ9J6nKEBvB4DnNLIVWSgARns'+
  '/8wR2SiRS7MNACwTyrGvt9ts8p12PKFd'+
  'lqYTopNHR1Vf7XjfhQlVsAJdNiKdYmYV'+
  'oKlaRv85IfVunYzO0IKXsyl7JCUjCpoG'+
  '20f0a04COwfneQAGGwd5oa+T8yO5hzuy'+
  'Db/XcxxmK01EpqOyuxINew=='

pc = WXBizDataCrypt.new(app_id, session_key)
puts pc.decrypt(encrypted_data, iv)

这里执行这个测试代码是可以通过的,不过不要被它给蒙蔽了,实际生产环境里会有偶发的报错,错误比例大概是 5/1 的样子。在搜解决方案的时候遇到一个用 Node 的同学也有类似问题地址,求问最近有没有人遇到类似的问题,或者有啥解决线索提供没。 😭 拜谢~ 🙏

我们在一个生产小应用上使用了 Rails 5 API 模式作为后端,解密的代码我按照官方的例子进行改写,但好像没有遇到你所说的问题:

class WXBizDataCrypt
  def initialize(app_id, session_key)
    @app_id = app_id
    @session_key = Base64.decode64(session_key)
  end

  def decrypt(encrypted_data, iv)
    encrypted_data = Base64.decode64(encrypted_data)
    iv = Base64.decode64(iv)

    cipher = OpenSSL::Cipher::AES.new(128, :CBC)
    cipher.decrypt
    cipher.padding = 0
    cipher.key = @session_key
    cipher.iv  = iv
    data = cipher.update(encrypted_data) << cipher.final
    result = JSON.parse(data[0...-data.last.ord])

    raise '解密错误' if result['watermark']['appid'] != @app_id
    result
  end
end

我不确定你的问题是否来自 result = JSON.load(data[0...-data.last.ord]) 这里要将最后一部分填充去除,如果可以请帮忙做一下测试 😄

另外项目地址: https://github.com/bayetech/wechat_mall_applet_backend/blob/master/lib/wxbiz_data_crypt.rb
前端小程序地址:https://github.com/bayetech/wechat_mall_applet
顺便无耻求个 star

@huacnlee @gehao 多谢二位,去除填充的方法我这里也有试过,但是仍然会有零星的问题,不过确实失败率是下来了。我现在的解决方案是失败了就在客户端重新请求一遍 encrypted_data 和 iv 然后重新解析登录。

#3 楼 @lazybios 以前我搞过类似的事情,方法对了是一定能算对的

ericguo 另一个微信开发的 Ruby Gem: wechat-gate 提及了此话题。 03月22日 17:02

在这一步的开发中,一定要按照这样的顺序 1. 小程序请求 login,拿到 code 然后传给服务端;2.服务端拿到 code 到微信服务器拿到 sessionKey;3.然后小程序调用 getuserinfo 接口拿到 encryptedData,iv,然后给服务端;4.服务端拿到客户端的 encryptedData,vi 还有之前的 sessionKey 去解密得到 unionId 等用户信息;不然就会出现你这样的问题,你这种情况偶然出现的原因就是 你在服务端还未去获取 sessionKey 的时候你就去调用了 getuserinfo,有时候你会比服务端快,有时候你会比服务端慢,所以就出现了偶然性

DearX-dlx 回复

我是先后小程序执行:wx.loginwx.getUserInfo,拿到 codeencryptedDataiv 一起提交后端的,没有问题呢。

class WxBizDataCrypt
  attr_accessor :app_id, :session_key

  def initialize(app_id, session_key)
    @app_id = app_id
    @session_key = session_key
  end

  def decrypt(encrypted_data, iv)
    session_key = Base64.decode64(@session_key)
    encrypted_data= Base64.decode64(encrypted_data)
    iv = Base64.decode64(iv)

    cipher = OpenSSL::Cipher::AES128.new(:CBC)
    cipher.decrypt
    cipher.key = session_key
    cipher.iv = iv
    cipher.padding = 0

    decrypted_plain_text = cipher.update(encrypted_data) + cipher.final
    decrypted = JSON.parse(decrypted_plain_text.strip.gsub(/\u000f|\u0010/, ''))
    raise('Invalid Buffer') if decrypted['watermark']['appid'] != @app_id

    decrypted
  end
end

报错: JSON::ParserError (784: unexpected token at ''):

@Thomastar
这一行代码需要更新一下:

decrypted = JSON.parse(decrypted_plain_text.strip.gsub(/\u000f|\u0010/, ''))  

更新为

decrypted_plain_text = decrypted_plain_text.strip.gsub(/[\u0000-\u001F\u2028\u2029]/, '')
paicha 回复

你好 我也是按你这种方式做解密 为什么还是会偶然出现解密失败呢

我来挖下坟,根本原因就是在获取 code 之前获取了 iv 和 encryteddata,导致 secret_key 不匹配。 先调 wx.login 获取到 code 的之后再调 getUserInfo,然后把三个值传回后台就可以了 6 楼说得更清楚

DearX-dlx 回复

此乃正解

我也挖坟,切记官方文档这段话:

在回调中调用 wx.login 登录,可能会刷新登录态。此时服务器使用 code 换取的 sessionKey 不是加密时使用的 sessionKey,导致解密失败。建议开发者提前进行 login;或者在回调中先使用 checkSession 进行登录态检查,避免 login 刷新登录态。

liuminhan 求教, 支付宝小程序 AES 解密 提及了此话题。 09月20日 16:44

在解析 userinfo 时候遇到编码不对的情况时,加上 force_encoding 就可以了

result = JSON.parse(decrypted_plain_text.strip.force_encoding("utf-8").gsub(/[\u0000-\u001F\u2028\u2029]/, ''))
需要 登录 后方可回复, 如果你还没有账号请 注册新账号