Cleverio Smart Plug without cloud or app
I recently bought a Wi-fi connected smart plug from Cleverio, sold by the Swedish company Kjell & Co.
If you follow the official instructions, you end up with an app on your phone, that you use to pair and control the plug with.
After some curious investigation, I found that the products from Cleverio are in practise just rebranded Tuya devices. This made me happy, since it significantly increases the number of resources available on the web from others who have investigated the products.
I had two goals with this blog post:
- Be able to control the plug without the app. Preferably through some kind of web-based REST API.
- Block the plug from accessing the Internet, while still allowing it to work on my local network.
(1) is mostly because I want to be able to do whatever I want to with my home automation setup, and (2) is mostly for security reasons. Internet-connected IoT devices are not particularly well-known for their security.
Prior Work⌗
There are several interesting projects related to Tuya devices. The following are those that I’ve looked most into.
- TinyTuya: A Python library to communicate with Tuya devices.
- rust-tuyapi: A Rust library to communicate with Tuya devices.
- rust-async-tuyapi: An
async
Rust library, based onrust-tuyapi
above.
Find the ID and Key of the Plug⌗
The first step to take control over the plug is to extract it’s ID and secret key. These are needed to be able to communicate with the plug directly on the same LAN.
There are multiple ways to do this, you only need to use one.
What I Did⌗
Since I already had an unused rooted Android phone at home, I was inspired by this guide, and did the following:
- Installed an old version of the Smart Life app, namely version 3.6.1. You can’t use the newest version of the app, I tried. After downloading the APK, I installed it with
adb install xxxxxx.apk
. - Logged in with my Tuya account
- Paired my plug with my account
- Fetched the
/data/data/com.tuya.smartlife/shared_prefs/preference_global_keyeuXXXXXXXX.xml
file from the phone to my computer, and renamed it tocodes.xml
. (I usedadb shell
andcat
to get the file content) - Ran
transform.py
from the guide above. id
andkey
are the things needed to control your plug
Other Guides⌗
- Follow the instructions from TinyTuya. Briefly: register as a Tuya developer and download the information from their developer portal.
- Follow the instructions from tuya_code_extract. Briefly: install the (old) Smart Life app in an online Android emulator, and extract the keys from there.
REST API⌗
Since I wanted to control the plug in any way I wanted, I built a small REST API written in Rust using axum. To communicate with the device, I based it on rust-async-tuyapi.
The API supports multiple plugs - numbered from 0 and up - and requires the ID, key, and IP-address for each plug.
The source code for the project can be found on GitHub: https://github.com/zozs/tuya-web.
Briefly, it exposes the following endpoints:
POST /outlet/:outlet_id
: Toggles the plug between on and offsPUT /outlet/:outlet_id/true
: Turn on the plugPUT /outlet/:outlet_id/false
: Turn off the plugGET /outlet/:outlet_id
: Returns current state of the plug
This allows me to connect the POST
route to a regular button, in my case a Flic button. At the same time, I can have cronjobs that turns a plug on or off at certain times during the day. In my case, I control a growing light using the following Kubernetes CronJob.
apiVersion: batch/v1
kind: CronJob
metadata:
name: tuya-growing-light-on
spec:
schedule: "0 9 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: curl
image: quay.io/curl/curl
command: ["curl", "-v", "-X", "PUT", "http://tuya-web.default.svc.cluster.local/outlet/0/true"]
restartPolicy: OnFailure
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: tuya-growing-light-off
spec:
schedule: "0 17 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: curl
image: quay.io/curl/curl
command: ["curl", "-v", "-X", "PUT", "http://tuya-web.default.svc.cluster.local/outlet/0/false"]
restartPolicy: OnFailure
Investigate and Block Internet Access⌗
After finishing my API, I blocked the Internet access for the plug’s IP address in my router. For me, that was through the use of a firewall rule in PF.
#/etc/pf.conf
# Block smart plug from accessing internet
block in quick on guestnet from { 10.0.5.50 } to !(guestnet:network)
At the same time, I turned on logging of DNS requests in unbound
.
#/etc/unbound.conf
log-queries: yes
verbosity: 2
The logs are posted below, but to summarise:
- There are a lot of broadcasts on port 6667/UDP. It seems to be connected to auto discovery for the app to find the plug on the same network. It doesn’t seem to matter that I block these.
- The plug makes a new DHCP request every five minutes. I suspect this is due to it being unable to access the Internet. However, it doesn’t seem to cause any harm.
- It performs DNS requests to
m2.tuyaeu.com
, and then tries to connect to that IP. Presumably, this is the actual cloud service that is normally used to control the plug with the app.
Logs⌗
Mar 28 10:36:51 zone dhcpd[24647]: DHCPOFFER on 10.0.5.50 to 1c:90:ff:0a:72:88 via vlan2
Mar 28 10:36:51 zone dhcpd[24647]: DHCPREQUEST for 10.0.5.50 from 1c:90:ff:0a:72:88 via vlan2
Mar 28 10:36:51 zone dhcpd[24647]: DHCPACK on 10.0.5.50 to 1c:90:ff:0a:72:88 via vlan2
Mar 28 10:37:03 rule 0/(match) block in on vlan2: 10.0.5.50.59727 > 255.255.255.255.6667: udp 172
Mar 28 10:37:08 rule 0/(match) block in on vlan2: 10.0.5.50.59727 > 255.255.255.255.6667: udp 172
Mar 28 10:37:13 rule 0/(match) block in on vlan2: 10.0.5.50.59727 > 255.255.255.255.6667: udp 172
Mar 28 10:37:18 rule 0/(match) block in on vlan2: 10.0.5.50.59727 > 255.255.255.255.6667: udp 172
Mar 28 10:37:23 rule 0/(match) block in on vlan2: 10.0.5.50.59727 > 255.255.255.255.6667: udp 172
Mar 28 10:37:28 rule 0/(match) block in on vlan2: 10.0.5.50.59727 > 255.255.255.255.6667: udp 172
Mar 28 10:37:33 rule 0/(match) block in on vlan2: 10.0.5.50.59727 > 255.255.255.255.6667: udp 172
Mar 28 10:37:38 rule 0/(match) block in on vlan2: 10.0.5.50.59727 > 255.255.255.255.6667: udp 172
Mar 28 10:37:43 rule 0/(match) block in on vlan2: 10.0.5.50.59727 > 255.255.255.255.6667: udp 172
Mar 28 10:37:44 zone unbound: [85358:0] info: 10.0.5.50 m2.tuyaeu.com. A IN
Mar 28 10:37:44 rule 0/(match) block in on vlan2: 10.0.5.50.40188 > 35.156.42.116.8886: S 357465146:357465146(0) win 4380 <mss 1460>
Mar 28 10:37:46 rule 0/(match) block in on vlan2: 10.0.5.50.40188 > 35.156.42.116.8886: S 357465146:357465146(0) win 4380 <mss 1460>
Mar 28 10:37:48 rule 0/(match) block in on vlan2: 10.0.5.50.40188 > 35.156.42.116.8886: S 357465146:357465146(0) win 4380 <mss 1460>
Mar 28 10:37:48 rule 0/(match) block in on vlan2: 10.0.5.50.59727 > 255.255.255.255.6667: udp 172
Mar 28 10:37:50 rule 0/(match) block in on vlan2: 10.0.5.50.40188 > 35.156.42.116.8886: S 357465146:357465146(0) win 4380 <mss 1460>
Mar 28 10:37:52 rule 0/(match) block in on vlan2: 10.0.5.50.40188 > 35.156.42.116.8886: S 357465146:357465146(0) win 4380 <mss 1460>
Mar 28 10:37:53 rule 0/(match) block in on vlan2: 10.0.5.50.59727 > 255.255.255.255.6667: udp 172
Mar 28 10:37:54 rule 0/(match) block in on vlan2: 10.0.5.50.40188 > 35.156.42.116.8886: S 357465146:357465146(0) win 4380 <mss 1460>
Mar 28 10:37:56 rule 0/(match) block in on vlan2: 10.0.5.50.40188 > 35.156.42.116.8886: S 357465146:357465146(0) win 4380 <mss 1460>
Mar 28 10:37:58 rule 0/(match) block in on vlan2: 10.0.5.50.40188 > 35.156.42.116.8886: S 357465146:357465146(0) win 4380 <mss 1460>
Mar 28 10:37:58 rule 0/(match) block in on vlan2: 10.0.5.50.59727 > 255.255.255.255.6667: udp 172
Mar 28 10:38:00 rule 0/(match) block in on vlan2: 10.0.5.50.40188 > 35.156.42.116.8886: S 357465146:357465146(0) win 4380 <mss 1460>
Mar 28 10:38:03 rule 0/(match) block in on vlan2: 10.0.5.50.59727 > 255.255.255.255.6667: udp 172