-
-
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() |
Thanks for sharing your helpfull comments.
I found that I had to login again after the
reolink.clear_certs()and subsequentsleepwas 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. Here's the OpenWrt/acme compatible solution for the Reolink RL-820A device.
#!/bin/sh
#
no_proxy="*"
BASE_URL="https://YOUR_SSL_DNS_ADDRESS_OF_THE_DEVICE"
USERNAME="YOUR_USERNAME"
PASSWORD="YOUR_PASSWORD"
CRT_PATH="/etc/acme/*.DOMAIN.TLD/fullchain.cer"
KEY_PATH="/etc/acme/*.DOMAIN.TLD/*.DOMAIN.TLD.key"
SLEEP_DELAY=10
#
#
b64_encode_file() {
openssl base64 -in "$1" -A
}
clear_cert() {
echo "π§Ή Clear cert"
CLEAR_PAYLOAD='[{"cmd":"CertificateClear","action":0,"param":{}}]'
curl -sk -X POST "$BASE_URL/cgi-bin/api.cgi?cmd=CertificateClear&token=$TOKEN" \
-H "Content-Type: application/json" \
-d "$CLEAR_PAYLOAD" > /dev/null
}
get_byte_count() {
echo -n "${1}" | wc -c | tr -d ' '
}
login() {
echo "π Login"
LOGIN_PAYLOAD=$(cat <<EOF
[{
"cmd": "Login",
"param": {
"User": {
"userName": "$USERNAME",
"password": "$PASSWORD"
}
}
}]
EOF
)
RESPONSE=$(curl -sk -X POST "$BASE_URL/cgi-bin/api.cgi?cmd=Login" \
-H "Content-Type: application/json" \
-d "$LOGIN_PAYLOAD")
TOKEN=$(echo "$RESPONSE" | grep -o '"name" : "[^"]*' | cut -d'"' -f4)
if [ -z "${TOKEN}" ]; then
echo "[ERROR] Login failed, got no token. Stop."
exit 99
fi
echo "β
Login successful, token=[$TOKEN]"
}
logout() {
echo "πͺ Logout"
curl -sk "$BASE_URL/cgi-bin/api.cgi?cmd=Logout&token=$TOKEN" > /dev/null
}
upload_cert() {
echo "π€ Upload cert"
CRT_CONTENT=$(b64_encode_file "$CRT_PATH")
CRT_SIZE=$(get_byte_count "${CRT_CONTENT}")
KEY_CONTENT=$(b64_encode_file "$KEY_PATH")
KEY_SIZE=$(get_byte_count "${KEY_CONTENT}")
CERT_PAYLOAD=$(cat <<EOF
[{
"cmd": "ImportCertificate",
"action": 0,
"param": {
"importCertificate": {
"crt": {
"size": $CRT_SIZE,
"name": "fullchain.crt",
"content": "$CRT_CONTENT"
},
"key": {
"size": $KEY_SIZE,
"name": "server.key",
"content": "$KEY_CONTENT"
}
}
}
}]
EOF
)
curl -sk -X POST "$BASE_URL/cgi-bin/api.cgi?cmd=ImportCertificate&token=$TOKEN" \
-H "Content-Type: application/json" \
-d "$CERT_PAYLOAD" > /dev/null
}
#
#
#
login
clear_cert
echo "β³ Waiting for the device to restart web service"
sleep "$SLEEP_DELAY"
#
login
upload_cert
echo "β³ Waiting for the device to restart web service"
sleep "$SLEEP_DELAY"
#
logout
echo "β
Done."
#
exit 0
Cool thanks for sharing!
I had one problem with an E1 Pro: the certificate is not activated until a reboot.
Nice! Thanks for sharing
I found that I had to login again after the
reolink.clear_certs()and subsequentsleepwas executed. I simply added areolink.login()beforereolink.update_certs()In addition, the Let's Encrypt certs I am using seem to fail the SSL validation function, so I commented that out
I also disabled the warnings:
to clean up the output a bit