-
-
Notifications
You must be signed in to change notification settings - Fork 14
Google TV (aka Android TV) Remote Control
This page explains how we can pair with a remote Android/Google TV to then send commands (like changing the channel, the volume, etc). Thanks to @hubertlejaune for his tremendous help.
The Android TV (aka server
in this document) should have 2 open ports: 6466 and 6467.
To know more about the Android TV, we can enter the below Linux command:
openssl s_client -connect ANDROID_TV_IP:6467 -prexit -state -debug
Which will return some information, including the server's public certificate.
If you only want the server's public certificate:
openssl s_client -showcerts -connect 82.64.124.77:6467 </dev/null 2>/dev/null|openssl x509 -outform PEM > server.pem
The pairing protocol will happen on port 6467.
It's required to generate our own (client) certificate.
In PHP we can do it with the below code:
<?php
// the commande line is: php generate_key.php > client.pem
// certificate details (Distinguished Name)
// (OpenSSL applies defaults to missing fields)
$dn = array(
"commonName" => "atvremote",
"countryName" => "US",
"stateOrProvinceName" => "California",
"localityName" => "Montain View",
"organizationName" => "Google Inc.",
"organizationalUnitName" => "Android",
"emailAddress" => "[email protected]"
);
// create certificate which is valid for ~10 years
$privkey = openssl_pkey_new();
$cert = openssl_csr_new($dn, $privkey);
$cert = openssl_csr_sign($cert, null, $privkey, 3650);
// export public key
openssl_x509_export($cert, $out);
echo $out;
// export private key
$passphrase = null;
openssl_pkey_export($privkey, $out, $passphrase);
echo $out;
It will generate a file called client.pem
that contains both the public and the private keys for our client.
You need to open a TLS/SSL connection to the server using port 6467.
In PHP, you could use https://github.com/reactphp/socket:
<?php
use React\EventLoop\Factory;
use React\Socket\Connector;
use React\Socket\SecureConnector;
use React\Socket\ConnectionInterface;
require __DIR__ . '/./vendor/autoload.php';
$host = 'SERVER_IP';
$loop = Factory::create();
$tcpConnector = new React\Socket\TcpConnector($loop);
$dnsResolverFactory = new React\Dns\Resolver\Factory();
$dns = $dnsResolverFactory->createCached('8.8.8.8', $loop);
$dnsConnector = new React\Socket\DnsConnector($tcpConnector, $dns);
$connector = new SecureConnector($dnsConnector, $loop, array(
'allow_self_signed' => true,
'verify_peer' => false,
'verify_peer_name' => false,
'dns' => false,
'local_cert' => 'client.pem'
));
$connector->connect('tls://' . $host . ':6467')->then(function (ConnectionInterface $connection) use ($host) {
$connection->on('data', function ($data) use ($connection) {
$dataLen = strlen($data);
echo "data recv => ".$data." (".strlen($data).")\n";
// deal with the messages received from the server
});
// below we can send the first message
$connection->write(/* first message here */);
}, 'printf');
$loop->run();
?>
- (first) we send the length of the message (JSON string) on 4 bytes,
- (second) we send the message (JSON string) itself.
As soon as we are connected to the server, we send a PAIRING_REQUEST(10) message (type
= 10
).
The first message to send is:
{"protocol_version":1,"payload":{"service_name":"androidtvremote","client_name":"CLIENT_NAME"},"type":10,"status":200}
The server returns a PAIRING_REQUEST_ACK(11) message with type
is 11
and status
is 200
:
{"protocol_version":1,"payload":{},"type":11,"status":200}
Then the client replies with a OPTIONS(20) message (type
= 20
):
{"protocol_version":1,"payload":{"output_encodings":[{"symbol_length":4,"type":3}],"input_encodings":[{"symbol_length":4,"type":3}],"preferred_role":1},"type":20,"status":200}
The server returns a OPTIONS(20) message with type
is 20
and status
is 200
.
Then the client replies with a CONFIGURATION(30) message (type
= 30
):
{"protocol_version":1,"payload":{"encoding":{"symbol_length":4,"type":3},"client_role":1},"type":30,"status":200}
The server returns a CONFIGURATION_ACK(31) message with type is 31
and status
is 200
.
🎉 The code appears on the TV screen!
Then the client replies with a SECRET(40) message (type
= 40
):
{"protocol_version":1,"payload":{"secret":"encodedSecret"},"type":40,"status":200}
At this stage, the TV screen shows a code with 4 characters (e.g. 4D35).
To find the encodedSecret
:
- we use a SHA-256 hash;
- we add the client public key's
modulus
to the hash; - we add the client public key's
exponent
to the hash; - we add the server public key's
modulus
to the hash; - we add the server public key's
exponent
to the hash; - we add the last 2 characters of the code to the hash (in the example it's
35
).
The result of the hash is then encoded in base64.
The server returns a SECRET_ACK(41) message with type is 41
and status
is 200
, as well as an encoded secret that permits to verify – we didn't try to decode it, but it's probably the first 2 characters of the code:
{"protocol_version":1,"payload":{"secret":"encodedSecretAck"},"type":41,"status":200}
(you can find some Java code that produces pretty much the same)
Here is the related PHP code:
<?php
use React\EventLoop\Factory;
use React\Socket\Connector;
use React\Socket\SecureConnector;
use React\Socket\ConnectionInterface;
require __DIR__ . '/./vendor/autoload.php';
$host = 'SERVER_IP';
$loop = Factory::create();
$tcpConnector = new React\Socket\TcpConnector($loop);
$dnsResolverFactory = new React\Dns\Resolver\Factory();
$dns = $dnsResolverFactory->createCached('8.8.8.8', $loop);
$dnsConnector = new React\Socket\DnsConnector($tcpConnector, $dns);
// get the server's public certificate
exec("openssl s_client -showcerts -connect ".escapeshellcmd($host).":6467 </dev/null 2>/dev/null|openssl x509 -outform PEM > server.pem");
$connector = new SecureConnector($dnsConnector, $loop, array(
'allow_self_signed' => true,
'verify_peer' => false,
'verify_peer_name' => false,
'dns' => false,
'local_cert' => 'client.pem'
));
// return the message's length on 4 bytes
function getLen($len) {
return chr($len>>24 & 0xFF).chr($len>>16 & 0xFF).chr($len>>8 & 0xFF).chr($len & 0xFF);
}
// connect to the server
$connector->connect('tls://' . $host . ':6467')->then(function (ConnectionInterface $connection) use ($host) {
$connection->on('data', function ($data) use ($connection) {
$dataLen = strlen($data);
echo "data recv => ".$data." (".strlen($data).")\n";
// the first response from the server is the message's size on 4 bytes (that looks like a char to convert to decimal) – we can ignore it
// only look at messages longer than 4 bytes
if ($dataLen > 4) {
// decode the JSON string
$res = json_decode($data);
// check the status is 200
if ($res->status === 200) {
// check at which step we are
switch($res->type) {
case 11:{
// message to send:
// {"protocol_version":1,"payload":{"output_encodings":[{"symbol_length":4,"type":3}],"input_encodings":[{"symbol_length":4,"type":3}],"preferred_role":1},"type":20,"status":200}
$json = new stdClass();
$json->protocol_version = 1;
$json->payload = new stdClass();
$json->payload->output_encodings = [];
$encoding = new stdClass();
$encoding->symbol_length = 4;
$encoding->type = 3;
array_push($json->payload->output_encodings, $encoding);
$json->payload->input_encodings = [];
$encoding = new stdClass();
$encoding->symbol_length = 4;
$encoding->type = 3;
array_push($json->payload->input_encodings, $encoding);
$json->payload->preferred_role = 1;
$json->type = 20;
$json->status = 200;
$payload = json_encode($json);
$payloadLen = strlen($payload);
$connection->write(getLen($payloadLen));
$connection->write($payload);
break;
}
case 20:{
// message to send:
// {"protocol_version":1,"payload":{"encoding":{"symbol_length":4,"type":3},"client_role":1},"type":30,"status":200}
$json = new stdClass();
$json->protocol_version = 1;
$json->payload = new stdClass();
$json->payload->encoding = new stdClass();
$json->payload->encoding->symbol_length = 4;
$json->payload->encoding->type = 3;
$json->payload->client_role = 1;
$json->type = 30;
$json->status = 200;
$payload = json_encode($json);
$payloadLen = strlen($payload);
$connection->write(getLen($payloadLen));
$connection->write($payload);
break;
}
case 31:{
// when we arrive here, the TV screen displays a code with 4 characters
// message to send:
// {"protocol_version":1,"payload":{"secret":"encodedSecret"},"type":40,"status":200}
$json = new stdClass();
$json->protocol_version = 1;
$json->payload = new stdClass();
// get the code... here we'll let the user to enter it in the console
$code = readline("Code: ");
// get the client's certificate
$clientPub = openssl_get_publickey(file_get_contents("client.pem"));
$clientPubDetails = openssl_pkey_get_details($clientPub);
// get the server's certificate
$serverPub = openssl_get_publickey(file_get_contents("public.key"));
$serverPubDetails = openssl_pkey_get_details($serverPub);
// get the client's certificate modulus
$clientModulus = $clientPubDetails['rsa']['n'];
// get the client's certificate exponent
$clientExponent = $clientPubDetails['rsa']['e'];
// get the server's certificate modulus
$serverModulus = $serverPubDetails['rsa']['n'];
// get the server's certificate exponent
$serverExponent = $serverPubDetails['rsa']['e'];
// use SHA-256
$ctxHash = hash_init('sha256');
hash_update($ctxHash, $clientModulus);
hash_update($ctxHash, $clientExponent);
hash_update($ctxHash, $serverModulus);
hash_update($ctxHash, $serverExponent);
// only keep the last two characters of the code
$codeBin = hex2bin(substr($code, 2));
hash_update($ctxHash, $codeBin);
$alpha = hash_final($ctxHash, true);
// encode in base64
$json->payload->secret = base64_encode($alpha);
$json->type = 40;
$json->status = 200;
$payload = json_encode($json);
$payloadLen = strlen($payload);
$connection->write(getLen($payloadLen));
$connection->write($payload);
break;
}
}
}
}
});
// send the first message to the server
// {"protocol_version":1,"payload":{"service_name":"androidtvremote","client_name":"TEST"},"type":10,"status":200}
$json = new stdClass();
$json->protocol_version = 1;
$json->payload = new stdClass();
$json->payload->service_name = "androidtvremote";
$json->payload->client_name = "interface Web";
$json->type = 10;
$json->status = 200;
$payload = json_encode($json);
$payloadLen = strlen($payload);
// send the message size
$connection->write(getLen($payloadLen));
// send the message
$connection->write($payload);
}, 'printf');
$loop->run();
?>
Now that the client is paired with the server, we'll use port 6466 to send the commands.
Please, note we'll use an array of bytes for the commands.
An initial message must be sent:
[1,0,0,21,0,0,0,1,0,0,0,1,32,3,0,0,0,0,0,0,4,116,101,115,116]
The server will respond with an array of bytes that should start with [1,7,0
You must send two messages to execute one command.
The format is:
[1,2,0,{SIZE=16},0,0,0,0,0,0,0, {COUNTER} ,0,0,0, {PRESS=0} ,0,0,0,{KEYCODE}]
[1,2,0,{SIZE=16},0,0,0,0,0,0,0,{COUNTER+1},0,0,0,{RELEASE=1},0,0,0,{KEYCODE}]
The {KEYCODE}
can be found on https://developer.android.com/reference/android/view/KeyEvent.
For example, if we want to send a VOLUME_UP
:
[1,2,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24]
[1,2,0,16,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,24]
And here some PHP code:
<?php
use React\EventLoop\Factory;
use React\Socket\Connector;
use React\Socket\SecureConnector;
use React\Socket\ConnectionInterface;
require __DIR__ . '/./vendor/autoload.php';
$host = 'SERVER_IP';
$loop = Factory::create();
$tcpConnector = new React\Socket\TcpConnector($loop);
$dnsResolverFactory = new React\Dns\Resolver\Factory();
$dns = $dnsResolverFactory->createCached('8.8.8.8', $loop);
$dnsConnector = new React\Socket\DnsConnector($tcpConnector, $dns);
$connector = new SecureConnector($dnsConnector, $loop, array(
'allow_self_signed' => true,
'verify_peer' => false,
'verify_peer_name' => false,
'dns' => false,
'local_cert' => 'client.pem'
));
// convert the array of bytes
function toMsg($arr) {
$chars = array_map("chr", $arr);
return join($chars);
}
// connect to the server
$connector->connect('tls://' . $host . ':6466')->then(function (ConnectionInterface $connection) use ($host) {
$connection->on('data', function ($data) use ($connection) {
// convert the data received to an array of bytes
$dataLen = strlen($data);
$arr = [];
for ($i=0; $i<$dataLen;$i++) {
$arr[] = ord($data[$i]);
}
$str = "[".implode(",", $arr)."]";
echo "data recv => ".$data." ".$str." (".strlen($data).")\n";
// if we receive [1,20,0,0] it means the server sent a ping
if (strpos($str, "[1,20,0,0]") === 0) {
// we can reply with a PONG [1,21,0,0] if we want
// $connection->write(toMsg([1,21,0,0]));
}
else if (strpos($str, "[1,7,0,") === 0) {
// we can send the command, here it's a VOLUME_UP
$connection->write(toMsg([1,2,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24]));
$connection->write(toMsg([1,2,0,16,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,24]));
}
});
// send the first message (configuration) to the server
$arr = [1,0,0,21,0,0,0,1,0,0,0,1,32,3,0,0,0,0,0,0,4,116,101,115,116];
$connection->write(toMsg($arr));
}, 'printf');
$loop->run();
?>