登录
  • 人们都希望被别人需要 却往往事与愿违
  • 在民主国家, 最高原则是全民的利益而不是统治者的利益。服从民主国家的统治权并不会使人变为奴隶, 而是使人变为公民 @斯宾诺莎 (哲学家)

JetBrains License Server 原理及 40 行 Python 代码的实现

编程 Benny 小土豆 29404 次浏览 5707 字 52 个评论
文章目录 [显示]
这篇文章在 2018 年 08 月 31 日 11:08:55 更新了哦~

Repository:
开源地址

警告:
本文仅供学习参考,请勿用于其他用途。如果你喜欢 JetBrains 的产品,请购买正版授权。

近期 license server 升级,可以阅读官方的这篇文章,在 2018.2.1(2018 年第三季度左右发布)之后旧版服务器将无法使用,所以先别升级到 2018.2.1,留在 2018.2 吧。关于新的激活服务器,似乎有点难度,欢迎各位提供任何可能会有帮助的资料和工具(包括但不限于本地激活器,license server,以及相关介绍文章和抓包记录等),更多详情请参考文末记录

估计是个程序员都应该使用过 JetBrains 的 IDE(尤其是 Python 程序员吧)。JetBrains 的产品线非常丰富,PHP 的 PHPStorm,前端的 Webstorm,Python 的 Pycharm,Java 的 IntelliJ,Ruby 的 RubyMine,Go 的 Goland,数据库的 DataGrip……

几个月之前,我曾经尝试着把 Java 版本的 License Server 移植到 Python,但是失败了。但是今天我就成功了…… 好了不废话了,咱来一步一步的学习下怎么用 40 行代码写一个 JetBrains License Server

JetBrains License Server 激活流程

打开 IDE,依次点击 Help-Register,输入服务器地址,点击 Activate

JetBrains License Server原理及40行Python代码的实现

这时就会从 License Server 取得一个 Ticket 了。

友情提示:

如果你有 edu 邮箱的话,那快去申请免费正版授权吧,哪怕你没有 edu 邮箱,也可以试试什么注册 edu 邮箱的,比如说这个

抓包与分析

网上已经有人写好了几个版本的授权服务器,我参考了 Java 版的、golang 版的还有 JavaScript 版的。

于是我找了那个能用的 Java 版本的,自己搭建,然后尝试激活、抓包并过滤 http,进行分析。

通过追踪 HTTP 流我们能发现,IDE 向指定服务器发起了一个 GET 请求,请求中包含很多参数,之后服务器返回了一个像是散列值,还有一小串 XML

JetBrains License Server原理及40行Python代码的实现

IDE 请求:

  1. GET /rpc/obtainTicket.action?buildDate=20170719&buildNumber=2017.2.2+Build+CL-172.3968.17&clientVersion=4&hostName=xxxxxxm&machineId=51xxxxx20a-14d259f1fc94&productCode=cfc7xxxx978-a2a2-46fxxxx405&productFamilyId=cfxxxx-ae43-4978-a2a2-46feb1679405&salt=1506491409302&secure=false&userName=xxxx&version=2017200&versionNumber=2017200

服务器返回:

  1. <!-- 61b3a2f53ffxxxxxxxxx6b132e0270275d12434f217ba3231b5 -->
  2.  
  3. <ObtainTicketResponse><message></message><prolongationPeriod>607875500</prolongationPeriod><responseCode>OK</responseCode><salt>1506491409302</salt><ticketId>1</ticketId><ticketProperties>licensee=xxxxx licenseType=0 </ticketProperties></ObtainTicketResponse>

由此我们可以很清楚的了解到激活的步骤:请求 + 相应 + 验证,那么大体猜测一下应该是服务端持有一个私钥,对请求字符串进行加密,然后返回,客户端使用公钥验证。

验证猜想

为了验证我们的猜想,咱其实是要去读其他版本的源代码的…… 鉴于这个流程挺复杂,要求你会多门语言,所以咱就不演示了…… 总之,经过我的研读,我发现的细节是这样的:

服务端构造如下的字符串:

  1. <ObtainTicketResponse><message></message><prolongationPeriod>607875500</prolongationPeriod><responseCode>OK</responseCode><salt>1506491409302</salt><ticketId>1</ticketId><ticketProperties>licensee=xxxxx licenseType=0 </ticketProperties></ObtainTicketResponse>

其中 salt 是发起请求时的时间戳(单位毫秒),licensee 是发起请求的用户名(其实这个用户名是随便的,在代码里写死也是可以的)。

然后服务器使用 RSA with MD5 and PKCS1v15 进行加密,把加密后的内容转换为十六进制,加上 HTML 注释 <!-- hex -->(注意前后的两个空格),把十六进制 HTML 注释和构造的字符串一起返回,客户端使用 hex 和返回的字符串(以及公钥)进行验证。

其实验证猜想的这一步才是最复杂的,需要参考很多源代码做出适当的实验。

既然猜想已经得到了证实,那么剩下来就开始写了。基础的结构很简单,用 flask 搭建一个简易的服务器,用 cryptography 完成密码学相关的操作。

cryptography 完成 RSA 签名与十六进制转换

Python 中比较安全好用的密码学库是 cryptography(唉 好吧)、pycrypto(这货很久不更新了,不敢用),顺便说一句,进行安全随机盐散列函数的有 passlib

首先需要引入对应的库

  1. from cryptography.hazmat.backends import default_backend
  2. from cryptography.hazmat.primitives import hashes
  3. from cryptography.hazmat.primitives import serialization
  4. from cryptography.hazmat.primitives.asymmetric import padding

然后载入 key(文件形式)

  1. with open('jbls_private_key.pem') as f:
  2. private_key = serialization.load_pem_private_key(f.read(), password=None, backend=default_backend())

执行加密

  1. i = bytes(content)
  2. signature = private_key.sign(i, padding.PKCS1v15(), hashes.MD5())

转换二进制为十六进制表达

  1. binascii.hexlify(signature)

大概这么五行,我们就完成了加密与转换操作,剩下我们只要在 Flask 端做一下逻辑处理就好了。

Flask 进行逻辑处理

这一步也很简单,用@app.route()装饰器确定路由,构造字符串与返回。真的是很简单,大概就是如下这么十几行:

  1. @app.route("/rpc/obtainTicket.action")
  2. def obtain():
  3. salt = request.args.get('salt')
  4. user_name = request.args.get('userName')
  5. content = "<ObtainTicketResponse><message></message>" + \
  6. "<prolongationPeriod>607875500</prolongationPeriod><responseCode>OK</responseCode>" + \
  7. "<salt>" + salt + "</salt><ticketId>1</ticketId>" \
  8. "<ticketProperties>licensee=" + user_name + "\tlicenseType=0\t</ticketProperties>" \
  9. "</ObtainTicketResponse>"
  10. verification_hex = hex_signature(content)
  11. return "<!-- " + verification_hex + " -->\n" + content

通过 Flask 的 requests 拿到 salt 和 username,构造 XML,调用函数,返回 hex 以及 XML。

所以加起来,整体代码真的只有不到 40 行。就算把 KEY 嵌入到源代码中,也只是 43 行而已……

最终结果

服务端的请求日志

JetBrains License Server原理及40行Python代码的实现

客户端显示激活成功

JetBrains License Server原理及40行Python代码的实现

附录与 FAQ

监听 0.0.0.0

如果想要把这个服务部署在服务器上,那么最好就监听 0.0.0.0,只需要把 app.run() 写成 app.run(host='0.0.0.0') 即可。

持久运行

当然我们希望这个 Flask 程序能够长期运行而不退出,此时我们最好使用 supervisor 或者 systemd 啦,详细参考此篇《Linux 怎么让程序持续运行:简单说说几种好玩的办法》

示例源代码

本示例源代码可以到下面的地址查看:
开源地址

为啥不放到 GitHub 呢?被 DMCA Takedown 啦ε=ε=ε=┏(゜ロ゜;)┛

需要注意的是,需要使用 pip 安装 cryptography、flask,并且 jbls.py 同时支持 Python 2 和 Python 3.

可否提供一个有效的私钥

不好意思,不提供…… 至于我这个测试的私钥是在哪找的…… 你们猜呀。

如何生成 exe 文件

使用 pyinstaller 就可以了。

  1. pyinstaller -F demo.py

travis-ci

我们用 travis-ci 做持续集成测试,但是 travis-ci 并不是为了运行程序而准备的。但是我们这个 Flask 应用还必须得运行才能测试。那咋办呢?subprocess.Popen()?反正各种奇技淫巧都试过了,不好用的。劝大家还是试试app.test_client().get(url).data吧,这样就能获取到响应然后 assert 了。

 

新版 JetBrains License Server 备注

以下内容全文引用自 lanyus 评论

贵站居然没有过滤 html 的特殊字符?重新发一下吧,以下是原文:

Jetbrains 家产品线激活服务器认证从 2018.2 开始使用两个 handler 来处理,第一个 handler 会对老的签名方式进行认证(<!-- abcdef1234567890 --><ObtainTicketResponse></ObtainTicketResponse > 类样式),该 handler 会判断签名是否包含 hex 之外的字符。如果没有,按老版本方式来验证签名,如果存在 hex 之外字符则抛异常由自己接住后进入第二个 handler 来验证。
从版本 2018.2.1 开始便去掉了第一个 handler,改为只由第二个 handler 来验证,由此老的验证服务器全面失效。
来说说第二个 handler 的验证逻辑吧:
新的报文返回还是形同:<!-- sign_body --><xxxResponse></xxxResponse>,其中 < xxxResponse></xxxResponse > 这样的响应正文中相比较老版本添加 < serveruid>xyz</serveruid > 配置,这个 xyz 后文中有用到,不可缺少。
sign_body 格式较老版本的 hex 字符串变动很大,其示例格式如下:SHA1withRSA-xxxxxxxxxxxxxx-yyyyyyyyyyyyyyy,以 - 符号分隔,SHA1withRSA 对应 java 中签名所使用的算法(可换为 MD5withRSA 等),xxxxxxxxxxxxxx 部分是使用该算法以私钥签名的 bin -> base64 字符串,yyyyyyyyyyyyyyy 部分则是 pem 格式的证书字符串(可直接放证书正文,不换行)。
其中 yyyyyyyyyyyyyyy 证书必须由 Jetbrains 产品内置的一个 CN 为 License Server CA 的 CA 证书签发才有效,而且 yyyyyyyyyyyyyyy 证书中必须包含 CN=xyz.lsrv.jetbrains.com(其中 xyz 对应前文 < serveruid>xyz</serveruid > 部分所填)。
证书验证通过后才会取出证书中所包含的公钥来验证签名 <xxxResponse></xxxResponse > 响应正文(和 base64Decode(xxxxxxxxxxxxxx) 比对),验证通过后继续完成服务器认证(解析正文内容,逻辑同旧版)。
啰啰嗦嗦这么多,总结一下:如果我们想要自己造验证服务器,则需要拿到一张 License Server CA 签发的证书(包括私钥),而且这个证书被 Jetbrains 发现可能会被吊销!所以这个恐怕有些痴人说梦了。
对了 License Server CA 证书的内容由另一张 Jetprofile 签发的叫 prod3y 的证书公钥定时校验(数据格式同上文所述,不过正文部分是 CA 证书字符串),应该是防止被替换吧。
还有类似 idea.lanyus.com 这样的认证服务器域名黑名单也由这张 prod3y 证书公钥定时校验,也是怕被篡改吧。如果要替换 CA 要从替换 prod3y 证书开始,但那是破解的内容了,不如注册服务器优雅。
就这么多。

 

所以估计 License Server 这条路可能走不通了,只能通过 javaagent 这种方法修改客户端(IDE)代码然后达到目的了。


文章版权归原作者所有丨本站默认采用 CC-BY-NC-SA 4.0 协议进行授权 |
转载必须包含本声明,并以超链接形式注明原作者和本文原始地址:
https://dmesg.app/jbls.html
喜欢 (18)
分享:-)
关于作者:
If you have any further questions, feel free to contact me in English or Chinese.
发表我的评论
取消评论

                     

去你妹的实名制!

  • 昵称 (必填)
  • 邮箱 (必填,不要邮件提醒可以随便写)
  • 网址 (选填)
(52) 个小伙伴在吐槽
  1. 不错,key 怎么搞?提示一下,然后最近在学 py,go 和 Java 都算会吧,另外两个的源码地址能提供一下吗?学习学习! BTW, 不支持 2018.2.1 很蛋疼
    木头 2018-08-17 10:00 回复
    • 2018.2.1 以及之后的版本官方换了激活方式采用证书签名。这个东西不修改客户端的话基本无解。至少我没办法搞定。
      chenq2018-08-17 10:13 回复
    • Key 的话,最简单的办法自然就是去上面提的能用的 Java 版提取,或者去官方 License Server 里也能提取出来;
      另外两个 go 和 JavaScript 版本的嘛,被 DMCA Takedown 了(其实我也是被 Takedown 的之一),所以源代码我也么有 /(ㄒ o ㄒ)/~~
      Benny 小土豆 2018-08-17 10:13 回复
      • 我找了几个 key,复制过来,要不是 500 内部错误,就是无效,蛋疼
        木头 2018-08-17 15:06 回复
        • 最新版本的话,要修改 util.py 哦,而且不要改错了,比如少了上下的 -----。
          500 的话应该是你改错了 Key
          Benny 小土豆 2018-08-17 15:41
      • 算逑了,不折腾了,2018.2.1 也不用了
        木头 2018-08-17 15:45 回复
        • 噗 2018.2.1 当然是不可以的,暂时用回 2018.2 吧
          Benny 小土豆 2018-08-17 15:52
  2. 哈呀,java 作者表示受宠若惊
    chenq2018-08-17 09:38 回复
    • 啊呀,俺才是受宠若惊,竟然把原作者吸引过来了 (✿◡‿◡)
      Benny 小土豆 2018-08-17 10:10 回复
    • 还支持激活 JRebel!良心作者,支持下!
      移影残风 2018-09-14 21:10 回复
      • 还是要感谢 Java 版的作者呐
        Benny 小土豆 2018-09-15 12:30 回复
  3. 嗨,2018.2.1 已经推出了,当我按注册时,会显示出: "License Server response has not passed data integrity check: Invalid signature format". 请问,有没有办法解决呢?
    wayne92018-08-10 16:48 回复
    • 我暂时也没有解决方案。不知道 2018.2.1 改了什么。需要一个官方新版 license server 的抓包样本。
      Benny 小土豆 2018-08-10 16:51 回复
      • 2018.2.1 以及之后的版本官方换了激活方式采用证书签名。这个东西不修改客户端的话基本无解。至少我没办法搞定。
        chenq2018-08-17 10:14 回复
        • lanyus 的评论区找到这么一篇原理分析,至今只拿到了两个证书…… 对没有私钥 /(ㄒ o ㄒ)/~~
          Benny 小土豆 2018-08-17 10:20
  4. 报错如下: cannot import name certificate_transparency
    echoops2018-08-06 13:13 回复
    • 确定正确安装了 cryptography 吗? sudo pip install -U cryptography
      推荐使用 Python 3
      Benny 小土豆 2018-08-06 13:21 回复
    • 这个问题已经解决了,看日志貌似也返回 200 了,idea 还是激活失败。 Licence Server response has not passed data integrity check: Invalid signature format
      echoops2018-08-06 13:23 回复
      • 你需要替换一个能够激活的 key 哦,默认的那个是随机生成的假的 key
        Benny 小土豆 2018-08-06 13:25 回复
        • 嗯 谢谢回复
          echoops2018-08-06 13:28
  5. 你好,改了 private key 后报错 ERROR in app: Exception on /rpc/ping.action [GET] Traceback (most recent call last): File "/usr/lib64/python2.7/site-packages/flask/app.py", line 2292, in wsgi_app response = self.full_dispatch_request() File "/usr/lib64/python2.7/site-packages/flask/app.py", line 1815, in full_dispatch_request rv = self.handle_user_exception(e) File "/usr/lib64/python2.7/site-packages/flask/app.py", line 1718, in handle_user_exception reraise(exc_type, exc_value, tb) File "/usr/lib64/python2.7/site-packages/flask/app.py", line 1813, in full_dispatch_request rv = self.dispatch_request() File "/usr/lib64/python2.7/site-packages/flask/app.py", line 1799, in dispatch_request return self.view_functionsrule.endpoint File "licenseserver/jbls.py", line 82, in ping verification_hex = hex_signature(content) File "licenseserver/jbls.py", line 114, in hex_signature private_key = serialization.load_pem_private_key(KEY, password=None, backend=default_backend()) File "/usr/lib64/python2.7/site-packages/cryptography/hazmat/primitives/serialization.py", line 20, in load_pem_private_key return backend.load_pem_private_key(data, password) File "/usr/lib64/python2.7/site-packages/cryptography/hazmat/backends/openssl/backend.py", line 1014, in load_pem_private_key password, File "/usr/lib64/python2.7/site-packages/cryptography/hazmat/backends/openssl/backend.py", line 1198, in _load_key mem_bio = self._bytes_to_bio(data) File "/usr/lib64/python2.7/site-packages/cryptography/hazmat/backends/openssl/backend.py", line 436, in _bytes_to_bio data_char_p = self._ffi.new(&quot;char[]&quot;, data) TypeError: initializer for ctype 'char[]' must be a str or list or tuple, not unicode
    vampillia2018-08-06 00:08 回复
    • 可能是你的 key 贴错了,有图吗
      Benny 小土豆 2018-08-06 09:03 回复
      • key 确实错了,我改了下。报如下错误 "GET //rpc/obtainTicket.action?buildDate=20180327&buildNumber=2018.1.5+Build+IU-181.5281.24&clientVersion=5&hostName=vampillia&machineId=18a3f2fa-3a93-42e1-b6dc-137f9a3b431e&productCode=49c202d4-ac56-452b-bb84-735056242fb3&productFamilyId=49c202d4-ac56-452b-bb84-735056242fb3&salt=1533518810640&secure=false&userName=jinxin&version=2018100&versionNumber=2018100 HTTP/1.1" 404 -
        vampillia2018-08-06 09:30 回复
        • GET //r
          前面两个 //
          Benny 小土豆 2018-08-06 09:31
      • // 是怎么出来的,我没改过代码,就改了下 key
        vampillia2018-08-06 09:37 回复
        • 因为你请求的时候是 http://yourhost.com:5000//rpc... 这样的呗
          Benny 小土豆 2018-08-06 09:39
        • 谢谢了
          vampillia2018-08-06 09:44
  6. 嗨,2018.2 出版了,license server 不能使用了,请问有什么解决方式呢?
    wayne92018-07-26 09:38 回复
    • Starting with the 2018.2.1 release
      官方跳票了,2018.2 还是可以正常激活并且没有任何报错的啊是有警告的,估计要等下一个小版本更新吧……
      Benny 小土豆 2018-07-26 09:52 回复
      • 哈哈,对不起没发现, 太心急了,多谢楼主的回复。 :)
        wayne92018-07-26 10:05 回复
        • 没关系没关系,发现更新 2018.2 的时候我也很激动,然后发现官方大概是跳票了……
          Benny 小土豆 2018-07-26 10:37
      • 官方当时说的就是 2018.2 升级 server 但是没指定哪个版本. 顺便问一下老板, 这个打字冒星星是什么插件做的?
        auv2018-07-29 13:28 回复
        • 官方最开始是 2018.2,后来改到 2018.2.1。打字查看这个 commit
          Benny 小土豆 2018-07-29 14:16
  7. 我现在用的也是 Java 版,可是不会 Java~嘤嘤嘤 个人也是更喜欢 Python,没想到 在这看到了
    nels0n2018-07-07 10:09 回复
    • Python 版写起来更简洁~
      Benny 小土豆 2018-07-07 14:32 回复
  8. 您好,现在 PhpStorm 2018.1.6 已经出现 warning message: Outdated License Server Detected Your license ticket is obtained from an outdated version of the license server. To keep your product licensed, the license server requires an immediate update to the latest version. Read full details, and pass this information to the server administrator on your side. 请问有没有办法解决这个问题呢?
    wayne92018-06-20 11:57 回复
    • 2018.1.6 未能复现。
      Benny 小土豆 2018-06-20 20:58 回复
      • 官方说明 Read full details: https://www.jetbrains.com/license-server/outdated-ls/
        zhuisui2018-06-21 14:33 回复
        • Starting with the 2018.2 major release
          ,还要稍微等等,想要获得最新更新就 watch 下吧
          Benny 小土豆 2018-06-21 20:35
  9. phpstorm 和 goland 用的挺爽的
    冯小贤 2018-05-24 18:33 回复
    • ?? 不会 go
      Benny 小土豆 2018-05-24 21:42 回复
  10. clion 使用时占用内存 1G+,VS2017 500M,不算轻量吧。。 (也不算什么大不了的事情啦)
    billow2018-03-31 23:08 回复
    • JetBrains 的东西是有点占用内存啦,不过如果你想想 VS2017 安装完要多大空间,你再参考下 Clion…………
      不过,你是不是评论错博文了??
      Benny 小土豆 2018-03-31 23:39 回复
  11. pycrypto 确实旧了点,在安卓上 termux 编译时各种报错,不装的话 ykdl 又用不了,pip 都解决不了
    cok3302018-02-23 10:22 回复
    • pycrypto 上次更新于 2014 年 6 月,还是尽量不要用的啦。
      Benny 小土豆 2018-02-23 15:25 回复
  12. Jetbrain 是真的良心... 话说如果学 C/c++, 你推荐 vs 还是 clion?
    每次评论换一个 2018-02-22 01:09 回复
    • 如果是标准的 C/C++ 的话,这俩都可以。但是 CLion 更轻量级。
      Benny 小土豆 2018-02-22 08:24 回复
  13. JetBrains 还有 Visual Studio 插件(比如 ReSharper),以及团队协作工具(TeamCity, YouTrack, Upsource, Hub)呢?
    还有 Toolbox 管理你说的 IDE 们的安装? (日常万能?)
    布偶君 2018-02-07 08:04 回复
    • 是的的是日常万能?
      Benny 小土豆 2018-02-07 09:09 回复
  14. 7 小时前发布!
    布偶君 2018-02-06 17:45 回复
    • 代码发布于 24 小时之前!
      Benny 小土豆 2018-02-06 17:48 回复
您直接访问了本站! 莫非您记住了我的域名. 厉害~ 我倍感荣幸啊 嘿嘿