-
-
Save velzend/895c18d533b3992f3a0cc128f27c0894 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3 | |
""" | |
I wanted to replace the self-signed certificate with one signed by Let's Encrypt, and | |
did not want to perform this action manually. | |
I asked Reolink technical support, but they answered there is no API to manage certificates. | |
So I did some research and found the Reolink doorbell camera and probably other models | |
do support to upload your own certificates. | |
In developer tools I checked the API and found the annoying client side AES encryption | |
implementation where the key is generated/ rotated in the script during login. | |
To reveal the key and iv, I simply set a breakpoint on the login logic and have the `e` and `t` values. | |
To decrypt the JSON payload I wrote the script below: | |
-------------------------------------------------------------------------------- | |
#!/usr/bin/env python3 | |
from crypto.Cipher import AES | |
import base64 | |
#e = "B642D317BD521D58", t = "0D6A4261FCD46185" | |
key = b"B642D317BD521D58" | |
iv = b"0D6A4261FCD46185" | |
encrypted_payload = "Plha/2eKtaMXwqNXZAlawvZB88qw3KdkRpLrMRol2nh1EmKPQJN****" | |
decipher = AES.new(key, AES.MODE_CFB, IV=iv, segment_size=128) | |
decrypted_payload = decipher.decrypt(base64.b64decode(encrypted_payload)) | |
print(decrypted_payload.decode("UTF-8")) | |
-------------------------------------------------------------------------------- | |
Now I know the endpoints and payload of API requests. | |
Important notes: | |
The webservice in the doorbell camera only supports RSA certificates and not EC (Elliptic Curve, ec256 for example). | |
If you use this script the certificate and key filenames are hardcoded to `server.crt` and `server.key`, | |
but during testing I found using filenames that contains the FQDN the API fails. | |
Next, you need a certificate and key. | |
I use Cloudflare as DNS provider, and prefer Lego as ACME client. | |
To request the certificate from Let's Encrypt I used the Lego container image below: | |
docker run \ | |
-v "$(pwd)/.lego:/.lego" \ | |
-e "CF_DNS_API_TOKEN=***" \ | |
goacme/lego \ | |
--key-type="rsa4096" \ | |
--accept-tos \ | |
--email="***" \ | |
--domains="***" \ | |
--dns="cloudflare" \ | |
run | |
Next step is to update some values in the script below, schedule this | |
script to run periodically after the docker run above and | |
enjoy automatic certificate updates/ rotation. | |
""" | |
import requests | |
import os | |
import base64 | |
import time | |
import ssl | |
import sys | |
os.environ['no_proxy'] = '*' | |
base_url = "https://***" | |
username = "admin" | |
password = "****" | |
certificate_path = ".lego/***.crt" | |
key_path = ".lego/***.key" | |
class Reolink(object): | |
def __init__(self, **kwargs): | |
self.base_url = kwargs.pop('base_url', None) | |
self.username = kwargs.pop('username', None) | |
self.password = kwargs.pop('password', None) | |
self.token = None | |
def login(self): | |
login_req = [{"cmd":"Login", | |
"param": {"User": {"userName": self.username, | |
"password": self.password} | |
} | |
} | |
] | |
url = f'{self.base_url}/cgi-bin/api.cgi?cmd=Login' | |
login_resp = requests.post(url=url, json=login_req, verify=False) | |
login_data = login_resp.json() | |
self.token = login_data[0]['value']['Token']['name'] | |
print(f"Login was succesfull, got token: {self.token}") | |
return self.token | |
def verify_ssl_certificate(self): | |
try: | |
response = requests.get(self.base_url) | |
response.raise_for_status() | |
print(f"Certificate for {self.base_url} is valid.") | |
return True | |
except ssl.SSLCertVerificationError as err: | |
print(f"Certificate verification failed for {self.base_url}, error: {err}", file=sys.stderr) | |
return False | |
def clear_certs(self): | |
url = f"{self.base_url}/cgi-bin/api.cgi?cmd=CertificateClear&token={self.token}" | |
clear_req = [{ | |
"cmd": "CertificateClear", | |
"action": 0, | |
"param": {} | |
}] | |
clear_certs_resp = requests.post(url=url, json=clear_req, verify=False) | |
clear_certs_data = clear_certs_resp.json | |
return clear_certs_data | |
def update_certs(self, certificate_path, key_path): | |
crtfile_stats = os.stat(certificate_path) | |
crt_filesize = crtfile_stats.st_size | |
with open(certificate_path, "rb") as crt_file: | |
b64_crt = base64.b64encode(crt_file.read()) | |
keyfile_stats = os.stat(key_path) | |
key_filesize = keyfile_stats.st_size | |
with open(key_path, "rb") as key_file: | |
b64_key = base64.b64encode(key_file.read()) | |
cert_req = [{ | |
"cmd": "ImportCertificate", | |
"action": 0, | |
"param": { | |
"importCertificate": { | |
"crt": { | |
"size": crt_filesize, | |
"name": "server.crt", | |
"content": b64_crt.decode("UTF-8") | |
}, | |
"key": { | |
"size": key_filesize, | |
"name": "server.key", | |
"content": b64_key.decode("UTF-8") | |
} | |
} | |
} | |
} | |
] | |
url = f"{self.base_url}/cgi-bin/api.cgi?cmd=ImportCertificate&token={self.token}" | |
update_certs_resp = requests.post(url=url, json=cert_req, verify=False) | |
update_certs_data = update_certs_resp.json | |
return update_certs_data | |
def logout(self): | |
url = f"{self.base_url}/cgi-bin/api.cgi?cmd=Logout&token={self.token}" | |
logout_resp = requests.get(url=url, verify=False) | |
logout_data = logout_resp.json | |
print(f"Logout was succesfull, got response: {logout_data}") | |
return logout_data | |
def main(): | |
reolink = Reolink(base_url=base_url, | |
username=username, | |
password=password) | |
reolink.login() | |
reolink.clear_certs() | |
# the doorbell will restart the internal web daemon | |
time.sleep(5) | |
reolink.update_certs(certificate_path=certificate_path, | |
key_path=key_path) | |
# the doorbell will restart the internal web daemon | |
time.sleep(5) | |
reolink.logout() | |
if not reolink.verify_ssl_certificate(): | |
exit(1) | |
if __name__ == "__main__": | |
main() |
I found that I had to login again after the
reolink.clear_certs()
and subsequentsleep
was executed. I simply added areolink.login()
beforereolink.update_certs()
Thanks so much, I did the same and got things working.
No need to clear the certificate on my rlc-1212a and rlc-823a it updates like a charm.
Thank you all, for your comments and @velzend for your script and your time.
Thanks for this. Worked well for the RLC-820A with some of the modifications above:
- Adding a login before updating the cert, as clearing them logs the user out
- Increasing the sleep time. The camera sometimes will take up to 7/8s to cycle. I went with 10s and that seems to be sufficient in all cases (tested with a loop of 30 times with no failures)
I'm using the certs generated by acme.sh for Lets Encrypt certs: https://github.com/acmesh-official/acme.sh
Even though these are .pem, they work fine. I'm using the fullchain.pem and key.pem, which get passed as a .crt and .key in the POST request.
A local DNS record for a domain I own, but has no public facing records works great. Remember if you run a Pi-hole or similar, you will need to register the record there.
I wanted to come back here and say that for whatever reason reolink.verify_ssl_certificate()
could not re-connect to the cameras. I confirmed that it is the requests module having a problem. Commenting out the reolink.verify_ssl_certificate()
lines allows the script to run without issue and it does still update the cameras.
Like @Chouhada I also had to increase the timeout. I have a handful of PTZ cameras from reolink and so far this script works like a charm
@Chouhada Hi, can you share your script for the acme platform. Thanks!
I had one problem with an E1 Pro: the certificate is not activated until a reboot.
Thanks for sharing your helpfull comments.