Skip to content

Instantly share code, notes, and snippets.

@interfect
Last active April 15, 2024 06:11
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save interfect/5f68381d55658d334e2bc4619d796476 to your computer and use it in GitHub Desktop.
Save interfect/5f68381d55658d334e2bc4619d796476 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
#!/usr/bin/env bash
# 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 jq 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
@simon816
Copy link

Can confirm this works for the WiFi hotspot mode too

@savagebread
Copy link

Works perfectly on windows

However, on macos I get this error:

node:internal/crypto/cipher:79
  return method(data, format, type, passphrase, buffer, padding, oaepHash,
      ^
Error: error:0408F08D:rsa routines:pkey_rsa_ctrl:invalid padding mode
  at Object.publicEncrypt (node:internal/crypto/cipher:79:12)
  at [stdin]:6:30
  at Script.runInThisContext (node:vm:129:12)
  at Object.runInThisContext (node:vm:305:38)
  at node:internal/process/execution:76:19
  at [stdin]-wrapper:6:22
  at evalScript (node:internal/process/execution:75:60)
  at node:internal/main/eval_stdin:29:5
  at ReadStream.<anonymous> (node:internal/process/execution:213:5)
  at ReadStream.emit (node:events:527:28) {
 opensslErrorStack: [
  'error:06089093:digital envelope routines:EVP_PKEY_CTX_ctrl:command not supported'
 ],
 library: 'rsa routines',
 function: 'pkey_rsa_ctrl',
 reason: 'invalid padding mode',
 code: 'ERR_OSSL_RSA_INVALID_PADDING_MODE'
}
Node.js v17.8.0

@interfect
Copy link
Author

@BreakingBr3ad Node 17.8 is documented to support crypto.constants.RSA_PKCS1_PADDING as a padding for publicEncrypt. But clearly it doesn't work for you.

It could be that having an oaepHash set but using RSA_PKCS1_PADDING instead of RSA_PKCS1_OAEP_PADDING is not actually a sensible combination, and so sometimes doesn't work.

Can you try it with the oaepHash line removed, and also with RSA_PKCS1_PADDING changed to RSA_PKCS1_OAEP_PADDING, and see which if any of those work? I suspect removing the hash function name might be the right one, because if we're really using RSA_PKCS1_PADDING we shouldn't ever actually use an OAEP hash function. See for example this SO answer that explains what these different kinds of padding are. PKCS1 just makes the message long enough to actually encrypt by adding bytes that aren't checked, and OAEP adds specific-valued bytes from the hash that are checked. I think.

@paulcollett
Copy link

paulcollett commented Apr 15, 2022

Thanks for this. I was able to get this working over wifi, and commenting out oaepHash: "sha256" fixed the node issue for me on MACOS.

Question - Is there anyway to set the displayed time / weather / location, or is that limited to the Google Home app setup?

@interfect
Copy link
Author

I'm not sure how to do that configuration @paulcollett. My Chromecast set up with this method doesn't seem to expose the relevant settings page if you wander by with a Google Home app later on, and I haven't worked out what's different about the initial configuration that causes that.

I've commented out that line in the script so it should work out of the box for more people, hopefully.

@initstring
Copy link

You are a hero. Thanks so much for this!

@initstring
Copy link

Just a note: On Fedora, I had to replace all instances of nodejs with node.

@asafamr
Copy link

asafamr commented Mar 6, 2023

worked in 2023 too 🎉 (replaced nodejs with node, node v 16.19.1, ubuntu, hotspot, third version of the gist)
THANKS!

@Cyanatide
Copy link

Hey!
On my chromecast with GoogleTV (gen4), here is the response from eureka_info:

{"name":"Chromecast3943","net":{"ethernet_connected":false,"ip_address":"","online":false},"setup":{"setup_state":0,"ssid_suffix":"ytb","tos_accepted":true},"version":0}

There is no public_key! so the script fails later trying to encrypt the password. Did I miss something? Or is it due to an update on the chromecast side?

@interfect
Copy link
Author

@Cyanatide Are you sure it isn't set up already maybe? How did it get "tos_accepted":true?

Or maybe the protocol has indeed changed.

@Cyanatide
Copy link

@interfect I'm sure I'm always in the setup. After the reset of the CCwGTV, I've just paired the remote and to get the page showing QRCode to be scanned with Google Home. Hotspot is enabled only from this page, and only this page provide the wifi password. So... I've no idea why "tos_accepted":true 😄
But yes, I'm pretty sure things have change with this new chromecast (tested you script with a Chromecast gen3 yesterday and was perfectly working). I've managed to get the public key with this command:
openssl s_client -showcerts -connect SERVER_IP:6467 </dev/null 2>/dev/null|openssl x509 -outform PEM > server.pem
but can't manage to encrypt properly my password with it, CCwGTV refuse it, not sure why... Will continue debugging.

@interfect
Copy link
Author

Yeah, if there's a QR code from the cast device to the set up device that you need to scan then there's definitely a different process going on. Maybe the public key is in the QR code? The device isn't in contact with Google yet, so either the QR code has what is needed to do the setup or it has something you send to Google to get back something from them to do the setup.

@3-w-c
Copy link

3-w-c commented Dec 24, 2023

Amazing, thank you so much! 🎉

Worked for me on hotspot also. NB if you get "can't connect to server" from cURL, make sure you don't have a VPN active.

@circuits-of-time
Copy link

circuits-of-time commented Jan 9, 2024

Thanks for posting the script, it's very helpful.

In case anyone else gets connection refused errors from curl when running the script, it seems older versions of the chromecast firmware only exposed wifi setup services over http on port 8008. I had this issue with a gen3 chromecast right out of the box (manufacture date 05/2021).

Old firmware:

"build_version": "123837"
"cast_build_revision": "1.32.123837"`
$ nmap 192.168.255.249
Starting Nmap 7.80 ( https://nmap.org/ ) at 2024-01-02 15:10 EST
Nmap scan report for 192.168.255.249
Host is up (0.0037s latency).
Not shown: 997 closed ports
PORT     STATE SERVICE
8008/tcp open  http
8009/tcp open  ajp13
9000/tcp open  cslistener

I changed the curl calls to use http on port 8008 and everything went smoothly. Once the chromecast was connected to wifi, it did an automatic firmware update and rebooted. Interestingly, after the firmware update the chromecast now exposed setup services on https port 8443. This port was then required to be used for setup on the new firmware, so the script above worked as is.

New firmware:

"build_version": "291998"
"cast_build_revision": "1.56.291998"
$ nmap 192.168.255.249
Starting Nmap 7.80 ( https://nmap.org/ ) at 2024-01-03 15:04 EST
Nmap scan report for 192.168.255.249
Host is up (0.0073s latency).
Not shown: 995 closed ports
PORT      STATE SERVICE
8008/tcp  open  http
8009/tcp  open  ajp13
8443/tcp  open  https-alt
9000/tcp  open  cslistener
10001/tcp open  scp-config

@Geologic9222
Copy link

Geologic9222 commented Feb 3, 2024

On Linux I'm having some trouble. Any thoughts on this?

Error: error:0680007B:asn1 encoding routines::header too long
    at Object.publicEncrypt (node:internal/crypto/cipher:79:12)
    at [stdin]:5:30
    at Script.runInThisContext (node:vm:129:12)
    at Object.runInThisContext (node:vm:307:38)
    at node:internal/process/execution:79:19
    at [stdin]-wrapper:6:22
    at evalScript (node:internal/process/execution:78:60)
    at node:internal/main/eval_stdin:30:5
    at Socket.<anonymous> (node:internal/process/execution:195:5)
    at Socket.emit (node:events:525:35) {
  opensslErrorStack: [
    'error:0688000D:asn1 encoding routines::ASN1 lib',
    'error:0688010A:asn1 encoding routines::nested asn1 error',
    'error:06800066:asn1 encoding routines::bad object header'
  ],
  library: 'asn1 encoding routines',
  reason: 'header too long',
  code: 'ERR_OSSL_ASN1_HEADER_TOO_LONG'
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment