-
-
Save 1mm0rt41PC/78e134d43e5badf3cc999bdf33e1fdd5 to your computer and use it in GitHub Desktop.
Set up a Chromecast from a Linux PC, without an Android or iOS mobile device and without Google Home
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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__": | |
check_dependencies() | |
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 | |
''' |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment