我住的小区使用了一个叫守望领域的智能门禁系统,可以通过手机App开小区门禁和单元门,但是用App开门需要经过四五步:打开App→进入开门界面→找到需要开的门→点击开门。

加上戴口罩时候解锁手机需要输入密码,导致整个流程非常耗时,经常需要站在小区门口和单元门口操作半天,有一段时间我甚至养成了携带实体门禁卡的习惯,实体门禁卡开门要快很多。

最近又开始忘带门禁卡,苦恼之余发现iOS在锁屏界面右划可以免解锁直接进入spotlight界面,这个界面可以添加捷径,如果能写一个捷径去调用守望领域App的API开门,就可以实现手机免解锁一键开门。

查找 API

首先需要通过Charles之类的软件查找App调用的API,配置Charles查看App请求的方式不再赘述,Google一下可以看到很多教程。直接看结果Charles的结果,可以看到api.lookdoor.cn是这个软件所请求的API域名。

打开软件发的请求非常多,经过操作和请求的对比可以看到,发送开门指令调用的API是:/func/hjapp/house/v1/pushOpenDoorBySn.json?equipmentId=xxxxxx 这个路径。

详细查看这个请求可以发现,equipmentId指的就是小区门的Id,接口使用cookie做认证,只要将cookie带上就可以模拟开门指令。

第一次尝试

打开iOS捷径App,创建一个新捷径,App调用API使用了POST请求,搜索Get contents of这个动作来实现发送POST请求。

通过Charles找到要开的门的URL填入,Method选择POST,Headers里填入Cookie进行认证,内容直接从Charles复制就可以,尝试运行,it works!

接下来把这个捷径添加到Spotlight界面,锁屏界面右划点一下,就可以实现一键开小区门禁,和打开App的四五步操作相比,确实省时省力。拿着新配好的捷径去上班,下班回到小区想试一把一键开门,结果又被困到门口了,上午还正常的捷径竟然失效了,打开一看API报登录超时,有可能是Cookie里的SESSION_ID过期了。

分析登录过程

再次用Charles抓包,分析登录相关的API,会发现主要是这两个:

  • /func/hjapp/user/v2/getPasswordAndKey.json:获取AES Key的API
  • /func/hjapp/user/v2/login.json?password=xxxxxx:登录API

通过分析,用时序图来表示这部分的交互逻辑:

登录过程清楚了,但是其中使用AES_KEY对密码进行加密的配置还是不清楚的,使用一个工具来尝试通过密文和AES_KEY来解密:http://tool.chacuo.net/cryptaes

输入密钥和密文,使用各种配置进行解密,当能够解出内容的时候,证明我们找到了加密的配置,可以看到BlockSize=128,padder使用的是pkcs7padding,加密模式是ECB。解密出来的字符并不是我们的密码,看着像是md5过的,用 echo -n xxxxxx | md5sum 把密码md5一下,对上了。看来服务端校验的是单次md5后的密码。

到这里登录逻辑已经搞清了,但是iOS捷径无法实现AES加密,单纯依托捷径来实现开门已经不可行了,需要搭建一个后端服务来计算密文。既然躲不过麻烦要搭建服务,不如把登录、开门整个流程都放在服务上,这样iOS捷径只需要一个请求就可以完成开门动作了。

考虑到登录开门的逻辑很简单,也就是3个HTTP请求+AES加密,直接在裸服务器上从0搭建步骤多成本高,要自己申请虚机、部署HTTP Server、Web App,还需要申请SSL证书,不仅初次搭建要搞个一两天,后续对机器和证书的维护也需要大量时间,成本极高。

最好是有服务能直接托管一段Python代码,第一时间想到的是Leancloud,一个Serverless服务提供商,但是实操过程中发现,由于政策要求Leancloud已经不提供域名了,绑定自己的域名也需要进行备案。这意味着只能选择一家海外Serverless服务商,看来看去AWS Lambda应该可以满足要求,试一下。

使用 AWS Lambda 搭建服务

AWS Lambda是一个Serverless服务,可以直接托管一段函数,省去配置服务和基础设施的麻烦。搭建一个Python的Serverless服务需要准备这么几件事:

  • 新建函数,编写代码
  • 添加API Gateway Trigger,确保函数可以通过HTTP请求调用
  • 配置函数的运行环境,增加一个层(Layer),这个层里打包进AES加密需要的cryptography和HTTP请求需要的requests

1. 函数代码

首先上代码,需要填写自己的手机号、md5后的密码、设备ID(可以用Charles获取)等字段,粘贴到Lambda的在线编辑器中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import json
import requests
import base64
import urllib.parse
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding

PHONE = ''
PASSWORD_MD5 = ''
DEVICE_ID = ''

def encrypt(key, msg):
cipher = Cipher(algorithms.AES(str.encode(key)), modes.ECB())
encryptor = cipher.encryptor()
padder = padding.PKCS7(128).padder()
msg = padder.update(str.encode(msg)) + padder.finalize()
ct = encryptor.update(msg) + encryptor.finalize()
return base64.b64encode(ct)

def lambda_handler(event, context):
resp = requests.post('https://api.lookdoor.cn:443/func/hjapp/user/v2/getPasswordAesKey.json?')
cookie = resp.headers['set-cookie']
aes_key = resp.json()['data']['aesKey']
password_encypted = urllib.parse.quote_plus(encrypt(aes_key, PASSWORD_MD5))

url = f'https://api.lookdoor.cn:443/func/hjapp/user/v2/login.json?password={password_encypted}&deviceId={DEVICE_ID}&loginNumber={PHONE}&equipmentFlag=1'
requests.post(url, headers={'cookie': cookie})

equipment_id = event['queryStringParameters']['equipment_id']
url = f'https://api.lookdoor.cn:443/func/hjapp/house/v1/pushOpenDoorBySn.json?equipmentId={equipment_id}'
resp = requests.post(url, headers={'cookie': cookie})
return resp.json()

代码首先通过API获取AES_KEY和SESSION_ID,然后使用AES_KEY对密码进行加密,接下来调用登录接口将获取的SESSION_ID绑定到当前账户,接下来根据请求传入的设备ID(门的ID)来发送开门指令。

点击Deploy部署,然后运行测试,会出现超时的报错,这是因为Lambda函数默认的执行器内存大小是128MB,超时时间是3s,在配置页面把内存改大一些,超时时间设置为10s就可以了。

2. 添加 API Gateway Trigger

一个Lambda函数可以被多种形式触发执行,因为要使用捷径通过HTTP请求调用,所以加一个API Gateway Trigger,添加后会自动为函数生成一个URL,通过这个URL就可以直接调用函数。

3. 添加包含依赖的 Layer

代码中使用了 requests 和 cryptography 这两个第三方库,Lambda不支持使用pip直接安装这些依赖,而是需要我们在把依赖打成zip包上传成为容器的一层Layer,添加到函数镜像中。需要注意的是,Lambda函数执行的环境是Linux,对于cryptography这个库需要打包Linux版的才可以正常使用。

由于日常使用的是Mac,所以在AWS上申请一台Ubuntu 20的EC2实例,登录实例后使用如下命令安装依赖,并打包成zip文件:

1
2
3
4
mkdir python
pip install -t python cryptography
pip install -t python requests
zip -r python/*

在AWS上创建一个新的Layer,并将生成的python.zip上传到Layer上。尝试通过URL访问写好的Lambda函数,可以看到开门指令已经成功下发。

配置iOS捷径

打开iOS捷径App,创建一个新捷径,搜索Get contents of这个动作,填入Lambda函数的URL和门的ID。由于API Gateway并没有配置认证,所以其他参数默认即可。如果有安全方面的顾虑,可以自己实现一个简单的Token认证或添加Lambda提供的JWT认证。点击执行,接口返回成功,证明整个流程已经跑通,以后就可以用这个捷径给自己和外卖小哥开门了。

总结

一开始本想用自定义一个iOS捷径的方式来实现一键开门禁,但为了实现SESSION_ID自动更新,不得不基于AWS Lambda搭了一个后端服务来模拟App的行为,所幸AWS Lambda提供了低成本的构建方案,包括搭建服务和配置SSL证书都可以几乎0成本的完成,免费套餐政策也能让这个服务长期跑着而不产生任何实际花费。