-
-
Save 1mm0rt41PC/78e134d43e5badf3cc999bdf33e1fdd5 to your computer and use it in GitHub Desktop.
| #!/usr/bin/env python3 | |
| # castanet.py: Script to connect a chromecast to a WiFi network. | |
| # | |
| # Allows you to put your Chromecast on WiFi and do Chromecast initial setup | |
| # without using the Google Home app at all, just using a normal computer. | |
| # | |
| # You do need your Chromecast to be on Ethernet, or (untested) to join its setup WiFi | |
| # network with your PC, and you also need to find out its IP yourself with e.g. | |
| # Wireshark. | |
| # | |
| # | |
| # Since 2025-03-09 the CA of Google for Chromecast is expired (see https://www.reddit.com/r/Chromecast/comments/1j7lhrs/comment/mgy1a88/ ) | |
| # To enable Cast from android: | |
| # 1) enable adb | |
| # 2) adb shell am start-activity -a com.google.android.gms.cast.settings.CastSettingsCollapsingDebugAction | |
| import os | |
| import sys | |
| import json | |
| import time | |
| import subprocess | |
| import requests | |
| import base64 | |
| from cryptography.hazmat.backends import default_backend | |
| from cryptography.hazmat.primitives.asymmetric import padding | |
| from cryptography.hazmat.primitives import serialization | |
| # Disable SSL warnings for self-signed certificates | |
| import urllib3 | |
| urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) | |
| try: | |
| import requests | |
| import json | |
| from cryptography.hazmat.primitives import serialization | |
| except ImportError as e: | |
| print(f"Error: Missing dependency - {e}") | |
| print("Install required packages with: pip install requests cryptography") | |
| sys.exit(1) | |
| def main(): | |
| # STEP0 => Connect to the chromecast temporary hotspot wifi | |
| chromecast_ip = '192.168.255.249' | |
| wifi_ssid = 'IOT' | |
| wifi_auth_number = 7 # PSK | |
| wifi_cipher_number = 4 # WPA2 | |
| wifi_password = '******' | |
| chromecast_name = 'MY-CAST' | |
| print(f"Connecting {chromecast_ip} to {wifi_ssid} with password {wifi_password}") | |
| # Base URL for API requests | |
| base_url = f"https://{chromecast_ip}:8443/setup" | |
| # Get the device's public key | |
| info_response = requests.get(f"{base_url}/eureka_info", verify=False) | |
| info_json = info_response.json() | |
| chromecast_pubkey = info_json["public_key"] | |
| if wifi_auth_number != '': | |
| # Scan for WiFi networks | |
| requests.post(f"{base_url}/scan_wifi", verify=False) | |
| print("Scanning for WiFi networks...") | |
| time.sleep(20) # Wait for scan to complete | |
| # Get scan results | |
| wifi_response = requests.get(f"{base_url}/scan_results", verify=False) | |
| wifi_json = wifi_response.json() | |
| # Find our network | |
| wifi_network = None | |
| for network in wifi_json: | |
| if network["ssid"] == wifi_ssid: | |
| wifi_network = network | |
| break | |
| if not wifi_network: | |
| print(f"Error: Could not find WiFi network '{wifi_ssid}'", file=sys.stderr) | |
| sys.exit(1) | |
| wifi_auth_number = wifi_network["wpa_auth"] | |
| wifi_cipher_number = wifi_network["wpa_cipher"] | |
| print(json.dumps(wifi_network, indent=2)) | |
| # Encrypt the password for the device | |
| encrypted_key = encrypt_password(wifi_password, chromecast_pubkey) | |
| # Generate the command to connect | |
| connect_command = { | |
| "ssid": wifi_ssid, | |
| "wpa_auth": wifi_auth_number, | |
| "wpa_cipher": wifi_cipher_number, | |
| "enc_passwd": encrypted_key | |
| } | |
| # And the command to save the connection | |
| save_command = {"keep_hotspot_until_connected": True} | |
| # Send the commands | |
| headers = {"content-type": "application/json"} | |
| connect_response = requests.post( | |
| f"{base_url}/connect_wifi", | |
| headers=headers, | |
| data=json.dumps(connect_command), | |
| verify=False | |
| ) | |
| print("Connect response: %s", connect_response.status_code) | |
| print("Connect Headers: %s", connect_response.headers) | |
| save_response = requests.post( | |
| f"{base_url}/save_wifi", | |
| headers=headers, | |
| data=json.dumps(save_command), | |
| verify=False | |
| ) | |
| print("Connect response: %s", save_response.status_code) | |
| print("Connect Headers: %s", save_response.headers) | |
| print("Connect Headers: %s", save_response.text) | |
| # Set the Chromecast name if provided | |
| if chromecast_name: | |
| rename_command = { | |
| "name": chromecast_name, | |
| "opt_in": { | |
| "crash": False, | |
| "stats": False, | |
| "opencast": False | |
| } | |
| } | |
| rename_response = requests.post( | |
| f"{base_url}/set_eureka_info", | |
| headers=headers, | |
| data=json.dumps(rename_command), | |
| verify=False | |
| ) | |
| print(f"Renamed Chromecast to '{chromecast_name}'. Response:", rename_response.status_code) | |
| print("\nConnection commands sent successfully!") | |
| print(""" | |
| Additional commands you can run: | |
| - To see device info: | |
| curl --insecure --tlsv1.2 --tls-max 1.2 https://${CHROMECAST_IP}:8443/setup/eureka_info | jq . | |
| - To list known networks: | |
| curl --insecure --tlsv1.2 --tls-max 1.2 https://${CHROMECAST_IP}:8443/setup/configured_networks | jq . | |
| - To forget a network: | |
| curl --insecure --tlsv1.2 --tls-max 1.2 -H "content-type: application/json" -d '{"wpa_id": 0}' https://${CHROMECAST_IP}:8443/setup/forget_wifi | |
| - To set name and opt out of things: | |
| curl --insecure --tlsv1.2 --tls-max 1.2 -H "content-type: application/json" -d '{"name": "NovakCast5000", "opt_in": {"crash": false, "stats": false, "opencast": false}}' https://${CHROMECAST_IP}:8443/setup/set_eureka_info | |
| """) | |
| def encrypt_password(password, public_key_pem): | |
| """Encrypt the WiFi password with the Chromecast's public key""" | |
| # Format the key properly | |
| public_key_pem = f"-----BEGIN RSA PUBLIC KEY-----\n{public_key_pem}\n-----END RSA PUBLIC KEY-----" | |
| # Load the public key | |
| public_key = serialization.load_pem_public_key( | |
| public_key_pem.encode('utf-8'), | |
| backend=default_backend() | |
| ) | |
| # Encrypt the password | |
| encrypted_data = public_key.encrypt( | |
| password.encode('utf-8'), | |
| padding.PKCS1v15() | |
| ) | |
| # Return the base64 encoded result | |
| return base64.b64encode(encrypted_data).decode('utf-8') | |
| if __name__ == "__main__": | |
| main() | |
| ''' | |
| # castanet.sh: Script to connect a chromecast to a WiFi network. | |
| # | |
| # Allows you to put your Chromecast on WiFi and do Chromecast initial setup | |
| # without using the Google Home app at all, just using a normal Linux computer. | |
| # | |
| # You do need your Chromecast to be on Ethernet, or (untested) to join its setup WiFi | |
| # network with your PC, and you also need to find out its IP yourself with e.g. | |
| # Wireshark. | |
| set -e | |
| if [[ -z "${CHROMECAST_IP}" || -z "${WIFI_SSID}" || -z "${WIFI_PASSWORD}" ]] ; then | |
| echo 1>&2 "Usage: CHROMECAST_IP=\"XXX\" WIFI_SSID=\"XXX\" WIFI_PASSWORD=\"XXX\" ${0}" | |
| exit 1 | |
| fi | |
| if ! which curl >/dev/null 2>/dev/null ; then | |
| echo 1>&2 "Install curl to use this script!" | |
| exit 1 | |
| fi | |
| if ! which jq >/dev/null 2>/dev/null ; then | |
| echo 1>&2 "Install jq to use this script!" | |
| exit 1 | |
| fi | |
| if ! which nodejs >/dev/null 2>/dev/null ; then | |
| echo 1>&2 "Install nodejs to use this script!" | |
| exit 1 | |
| fi | |
| # Set VERBOSITY=-vvv to see Curl traffic happening | |
| if [[ -z "${VERBOSITY}" ]] ; then | |
| VERBOSITY=-s | |
| fi | |
| echo "Connecting ${CHROMECAST_IP} to ${WIFI_SSID} with password ${WIFI_PASSWORD}" | |
| # Get the device's public key | |
| INFO_JSON="$(curl ${VERBOSITY} --insecure --tlsv1.2 --tls-max 1.2 https://${CHROMECAST_IP}:8443/setup/eureka_info)" | |
| CHROMECAST_PUBKEY="$(echo "${INFO_JSON}" | jq -r '.public_key')" | |
| # Scan for and find the network we want to get the encryption parameters | |
| curl ${VERBOSITY} --insecure --tlsv1.2 --tls-max 1.2 -X POST https://${CHROMECAST_IP}:8443/setup/scan_wifi | |
| sleep 20 | |
| WIFI_JSON="$(curl ${VERBOSITY} --insecure --tlsv1.2 --tls-max 1.2 https://${CHROMECAST_IP}:8443/setup/scan_results)" | |
| WIFI_NETWORK_JSON="$(echo "${WIFI_JSON}" | jq ".[] | select(.ssid == \"${WIFI_SSID}\")")" | |
| WIFI_AUTH_NUMBER="$(echo "${WIFI_NETWORK_JSON}" | jq -r '.wpa_auth')" | |
| WIFI_CIPHER_NUMBER="$(echo "${WIFI_NETWORK_JSON}" | jq -r '.wpa_cipher')" | |
| echo "${WIFI_NETWORK_JSON}" | |
| # Encrypt the password to the device | |
| # Encryption kernel by @thorleifjaocbsen | |
| # See <https://github.com/rithvikvibhu/GHLocalApi/issues/68#issue-766300901> | |
| ENCRYPTED_KEY="$(nodejs <<EOF | |
| let crypto = require('crypto'); | |
| let cleartext = "${WIFI_PASSWORD}"; | |
| let publicKey = "${CHROMECAST_PUBKEY}"; | |
| publicKey = "-----BEGIN RSA PUBLIC KEY-----\n"+publicKey+"\n-----END RSA PUBLIC KEY-----" | |
| const encryptedData = crypto.publicEncrypt({ | |
| key: publicKey, | |
| padding: crypto.constants.RSA_PKCS1_PADDING, | |
| // This was in the original thorleifjaocbsen code but seems nonsensical/unneeded and upsest some Nodes | |
| //oaepHash: "sha256", | |
| }, Buffer.from(cleartext)); | |
| console.log(encryptedData.toString("base64")); | |
| EOF | |
| )" | |
| # Generate the command to connect. | |
| CONNECT_COMMAND="{\"ssid\": \"${WIFI_SSID}\", \"wpa_auth\": ${WIFI_AUTH_NUMBER}, \"wpa_cipher\": ${WIFI_CIPHER_NUMBER}, \"enc_passwd\": \"${ENCRYPTED_KEY}\"}" | |
| # And the command to save the connection. | |
| # Include keep_hotspot_until_connected in case we are on the Chromecast's setup hotspot and not Ethernet. | |
| # See <https://github.com/rithvikvibhu/GHLocalApi/issues/88#issuecomment-860538447> | |
| SAVE_COMMAND="{\"keep_hotspot_until_connected\": true}" | |
| # Send the commands | |
| curl ${VERBOSITY} --insecure --tlsv1.2 --tls-max 1.2 -H "content-type: application/json" -d "${CONNECT_COMMAND}" https://${CHROMECAST_IP}:8443/setup/connect_wifi | |
| # Hope this one gets there before it can actually disconnect if we're using the setup hotspot? | |
| # Otherwise we have to use Ethernet or jump over to the target network and find the device again. | |
| # See <http://blog.brokennetwork.ca/2019/05/setting-up-google-chromecast-without.html?m=1> for a script that knows how to swap wifi networks but needs to be ported to use the current API. | |
| curl ${VERBOSITY} --insecure --tlsv1.2 --tls-max 1.2 -H "content-type: application/json" -d "${SAVE_COMMAND}" https://${CHROMECAST_IP}:8443/setup/save_wifi | |
| # To see it working, if you aren't kicked off the hotspot (or if you set the new CHROMECAST_IP in your shell): | |
| # | |
| # curl --insecure --tlsv1.2 --tls-max 1.2 https://${CHROMECAST_IP}:8443/setup/eureka_info | jq . | |
| # | |
| # To list known networks: | |
| # | |
| # curl --insecure --tlsv1.2 --tls-max 1.2 https://${CHROMECAST_IP}:8443/setup/configured_networks | jq . | |
| # | |
| # To forget a newtwork: | |
| # | |
| # curl --insecure --tlsv1.2 --tls-max 1.2 -H "content-type: application/json" -d '{"wpa_id": 0}' https://${CHROMECAST_IP}:8443/setup/forget_wifi | |
| # | |
| # If you leave Ethernet plugged in, the Chromecast will ARP for its WiFi IP on | |
| # Etherenet and drop the WiFi connection! Unplug the Chromecast, and plug it in | |
| # again with no Ethernet, to get it to keep the WiFi connection up! | |
| # | |
| # Set Name and opt out of things: | |
| # | |
| # curl --insecure --tlsv1.2 --tls-max 1.2 -H "content-type: application/json" -d '{"name": "NovakCast5000", "opt_in": {"crash": false, "stats": false, "opencast": false}}' https://${CHROMECAST_IP}:8443/setup/set_eureka_info | |
| ''' |
It is helpful, thank you.
check_dependencies()is not defined, but I just removed that line, and it worked :)
Thank you! Fixed
It is helpful, thank you.
You're welcome
Hi, I'm having some trouble getting this to work with my "Chromecast with Google TV" and a Linux PC. So I connected the PC with the Chromecast hotspot and provided the correct SSID and password to the script but it kept complaining about the public key format, so I've debugged it a little bit, and figured that the response I get from https://CHROMECAST_IP:8443/setup/eureka_info is always:
{"name":"Chromecast8462","net":{"ethernet_connected":false,"ip_address":"","online":false},"setup":{"setup_state":0,"ssid_suffix":"ytb","tos_accepted":true},"version":0}
As you can see, it's missing the public key. Did this script stopped working or did I do something wrong?
I've also tried the original shell script, but as it calls the same URL, it didn't work either...
Hello!
The field public_key is always present, is it a new Chromecast ? I checked https://CHROMECAST_IP:8443/setup/eureka_info on my chromecast right now and it still shows the public_key
Hi, so I looked again and the device I got is a "Chromecast with Google TV". The Android version is 12. Does this mean it just won't work on these newer devices?
side note
also sorry for my late response, I thought I'd get an email when someone replied, but apparently I had disabled those in my GitHub account without rememberingThis works. Thank you.
I'm able to setup a 3rd generation from Windows PC 🥇
check_dependencies()is not defined, but I just removed that line, and it worked :)