Skip to content

Google TV (aka Android TV) Remote Control (v2)

Aymeric edited this page Jan 1, 2022 · 39 revisions

This page explains the protocol ATV Remote v2 (used since September 2021 by Google with its Remote Service v5): how we can pair with a remote Android/Google TV and how to send commands (like changing the channel, the volume, etc). Thanks to @hubertlejaune for his tremendous help.

Requirements

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 SERVER_IP:6467 -prexit -state -debug

Which will return some information, including the server's public certificate that we'll need later.

If you only want the server's public certificate:

openssl s_client -showcerts -connect SERVER_IP:6467 </dev/null 2>/dev/null|openssl x509 -outform PEM > server.pem

Note

Pairing

The pairing protocol will happen on port 6467.

Client's certificate

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.

Connection to the server

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();
?>

1) Send the Pairing message

We need to build the payload with an array of bytes:

  • 8, 2: it's the protocol version 2
  • 16, 200, 1: it's the status code OK
  • 82: it's the message tag
  • 43: it's the length of the message
  • 10: it's the service name tag
  • LENGTH_OF_NEXT_STRING, BYTES_OF_THE_STRING_THAT_IS_THE_SERVICE_NAME: it's the service name
    e.g. [21, 105,110,102,111,46,107,111,100,111,110,111,46,97,115,115,105,115,116,97,110,116], with 21 the size, and 105,110,102,111,46,107,111,100,111,110,111,46,97,115,115,105,115,116,97,110,116 for info.kodono.assistant
  • 18: it's the tag device name
  • LENGTH_OF_NEXT_STRING, BYTES_OF_THE_STRING_THAT_IS_THE_CLIENT_NAME: it's the client name
    e.g. [13, 105, 110, 116, 101, 114, 102, 97, 99, 101, 32, 119, 101, 98], with 13 the size, and 105, 110, 116, 101, 114, 102, 97, 99, 101, 32, 119, 101, 98 for interface web

So we need to send a first message with the size of our payload, and then the payload:

[ 45 ] 
[ 8, 2, 16, 200, 1, 82, 43, 10, 21, 105, 110, 102, 111, 46, 107, 111, 100, 111, 110, 111, 46, 97, 115, 115, 105, 115, 116, 97, 110, 116, 18, 13, 105, 110, 116, 101, 114, 102, 97, 99, 101, 32, 119, 101, 98 ]

The server acknowledges with the 2 messages, one that is the size, and then the message itself:

[ 7 ]
[ 8, 2, 16, 200, 1, 90, 0 ]

We can split the response:

  • 8, 2: it's the protocol version 2
  • 16, 200, 1: it's the status code OK (it could be 16, 144, 3 for ERROR, or 16, 145, 3 for BAD_CONFIGURATION)

2) Send the Option message

We respond with the below payload:

  • 8, 2: it's the protocol version 2
  • 16, 200, 1: it's the status code OK
  • 162: it's the message tag??
  • 1: ??
  • 8: it's the encoding output??
  • 10: ??
  • 4: the size??
  • 8: it's the tag type??
  • 3: it's the encoding type (0 for ENCODING_TYPE_UNKNOWN, 1 for ENCODING_TYPE_ALPHANUMERIC, 2 for ENCODING_TYPE_NUMERIC, 3 for ENCODING_TYPE_HEXADECIMAL, 4 for ENCODING_TYPE_QRCODE)
  • 16: it's the size tag??
  • 6: it's the symbol length??
  • 24: it's the preferred role tag??
  • 1: it's the preferred role (1 for ROLE_TYPE_INPUT)

So we need to send a first message with the size of our payload, and then the payload:

[ 16 ] 
[ 8, 2, 16, 200, 1, 162, 1, 8, 10, 4, 8, 3, 16, 6, 24, 1 ]

The server acknowledges with the 2 messages, one that is the size, and then the message itself:

[ 16 ]
[ 8, 2, 16, 200, 1, 162, 1, 8, 18, 4, 8, 3, 16, 6, 24, 1 ]

3) Send the Configuration message

We respond with the below payload:

  • 8, 2: it's the protocol version 2
  • 16, 200, 1: it's the status code OK
  • 242: it's the message tag??
  • 1: ??
  • 8: it's the encoding tag??
  • 10: ??
  • 4: it's the size??
  • 8: it's the type tag
  • 3: it's the protocol encoding (0 for ENCODING_TYPE_UNKNOWN, 1 for ENCODING_TYPE_ALPHANUMERIC, 2 for ENCODING_TYPE_NUMERIC, 3 for ENCODING_TYPE_HEXADECIMAL, 4 for ENCODING_TYPE_QRCODE)
  • 16: it's the size tag??
  • 6: it's the symbol length??
  • 16: it's the preferred role tag??
  • 1: it's the preferred role (1 for ROLE_TYPE_INPUT)

So we need to send a first message with the size of our payload, and then the payload:

[ 16 ] 
[ 8, 2, 16, 200, 1, 242, 1, 8, 10, 4, 8, 3, 16, 6, 16, 1 ]

The server acknowledges with the 2 messages, one that is the size, and then the message itself:

[ 8 ]
[ 8, 2, 16, 200, 1, 250, 1, 0 ]

The TV screen should display a code with 6 characters.

4) Send the secret

Encode the secret

We first need to encode the secret.

To find the encoded secret:

  • 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 (the one displayed on the TV screen) to the hash.

In Java, the function looks like that:

public byte[] computeAlphaValue(byte[] bArr) {
  PublicKey publicKey = this.clientCertificate.getPublicKey();
  PublicKey publicKey2 = this.serverCertificate.getPublicKey();
  verboseDebug("computeAlphaValue, nonce=" + bytesToHexString(bArr));
  if (!(publicKey instanceof RSAPublicKey) || !(publicKey2 instanceof RSAPublicKey)) {
      Log.e(TAG, "Expecting RSA public key");
      return null;
  }
  RSAPublicKey rSAPublicKey = (RSAPublicKey) publicKey;
  RSAPublicKey rSAPublicKey2 = (RSAPublicKey) publicKey2;
  try {
      MessageDigest instance = MessageDigest.getInstance("SHA-256");
      byte[] byteArray = rSAPublicKey.getModulus().abs().toByteArray();
      byte[] byteArray2 = rSAPublicKey.getPublicExponent().abs().toByteArray();
      byte[] byteArray3 = rSAPublicKey2.getModulus().abs().toByteArray();
      byte[] byteArray4 = rSAPublicKey2.getPublicExponent().abs().toByteArray();
      byte[] removeLeadingNullBytes = removeLeadingNullBytes(byteArray);
      byte[] removeLeadingNullBytes2 = removeLeadingNullBytes(byteArray2);
      byte[] removeLeadingNullBytes3 = removeLeadingNullBytes(byteArray3);
      byte[] removeLeadingNullBytes4 = removeLeadingNullBytes(byteArray4);
      verboseDebug("Hash inputs, in order: ");
      verboseDebug("   client modulus: " + bytesToHexString(removeLeadingNullBytes));
      verboseDebug("  client exponent: " + bytesToHexString(removeLeadingNullBytes2));
      verboseDebug("   server modulus: " + bytesToHexString(removeLeadingNullBytes3));
      verboseDebug("  server exponent: " + bytesToHexString(removeLeadingNullBytes4));
      verboseDebug("            nonce: " + bytesToHexString(bArr));
      instance.update(removeLeadingNullBytes);
      instance.update(removeLeadingNullBytes2);
      instance.update(removeLeadingNullBytes3);
      instance.update(removeLeadingNullBytes4);
      instance.update(bArr);
      return instance.digest();
  } catch (NoSuchAlgorithmException unused) {
      Log.e(TAG, "no sha-256 implementation");
      return null;
  }
}

In PHP:

// get the client's certificate
$clientPub = openssl_get_publickey(file_get_contents(__DIR__.'/client.pem'));
$clientPubDetails = openssl_pkey_get_details($clientPub);
// get the server's certificate
$serverPub = openssl_get_publickey($serverCertificate);
$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, 4));
hash_update($ctxHash, $codeBin);
$alpha = hash_final($ctxHash, true);

// change it to an array of bytes that will use in the payload
$alphaHex = bin2hex($alpha);
for ($i=0; $i<strlen($alphaHex); $i+=2) {
  array_push($payload, hexdec(substr($alphaHex, $i, 2)));
}

Send the encoded secret

We send it with the below payload:

  • 8, 2: it's the protocol version 2
  • 16, 200, 1: it's the status code OK
  • 194, 2, 34, 10: ??
  • 32: it's the size of the encoded secret*
  • THE_ENCODED_SECRET_ON_32_BYTES

The server replies with the same kind of encoding, because we're supposed to verify we're dealing with the correct server:

  • 8, 2: it's the protocol version 2
  • 16, 200, 1: it's the status code OK
  • several bytes…

Send commands

Now that the client is paired with the server, we'll use port 6466 to send the commands.

Three steps are required to send a command:

  1. Send the 1st configuration message
  2. Send the 2nd configuration message
  3. Send the command

1st Configuration Message

Two messages must be sent.

This is the first one:

  • 10: tag
  • SIZE_OF_THE_WHOLE_MESSAGE
  • 8, 238, 4, 18: ??
  • SIZE_OF_THE_SUB_MESSAGE
  • 10: tag
  • SIZE_OF_MODEL_NAME
  • MODEL_NAME: e.g. Assistant Cloud becomes 65, 115, 115, 105, 115, 116, 97, 110, 116, 32, 67, 108, 111, 117, 100
  • 18: tag
  • SIZE_OF_VENDOR_NAME
  • VENDOR_NAME: e.g. Kodono becomes 75, 111, 100, 111, 110, 111
  • 24, 1, 34, 2, 49, 48: ??
  • 42: tag
  • SIZE_OF_PACKAGE_NAME
  • APP_NAME: e.g. info.kodono.assistant becomes 105,110,102,111,46,107,111,100,111,110,111,46,97,115,115,105,115,116,97,110,116
  • SIZE_OF_APP_VERSION
  • APP_VERSION: e.g. 1.0.0 becomes 49, 46, 48, 46, 48

And the second one:

  • 82 2 16 3: for {"remoteKeyInject":{"direction":"SHORT"}}

We first send the size, and then the payload. Example of a valid message:

[ 74 ]
[ 10, 73, 8, 238, 4, 18, 60, 10, 15, 65, 115, 115, 105, 115, 116, 97, 110, 116, 32, 67, 108, 111, 117, 100, 18, 6, 75, 111, 100, 111, 110, 111, 24, 1, 34, 2, 49, 48, 42, 21, 105, 110, 102, 111, 46, 107, 111, 100, 111, 110, 111, 46, 97, 115, 115, 105, 115, 116, 97, 110, 116, 5, 49, 46, 48, 46, 48 ]
[ 5 ] 
[ 82, 2, 16, 3 ]

The server returns:

[ 5 ]
[ 10, 3, 8, 255, 4 ]

2nd Configuration Message

Note: I had to add a 100ms delay before sending the second configuration message.

After the response from the server, we send a second payload:

  • 18, 3, 8, 238, 4: ??

We send the size first, and the payload:

[ 5 ] 
[ 18, 3, 8, 238, 4 ]

The server will respond with several messages (that may vary because they provided the server's name and the current application, e.g. 111,114,103,46,100,114,111,105,100,116,118,46,112,108,97,121,116,118 for org.droidtv.playtv):

[ 2 ]
[ 18,0 ]
[ 5 ]
[ 194, 2, 2, 8, 1, ]
[ 25 ]
[ 162, 1, 22, 10, 20, 98, 18, 111, 114, 103, 46, 100, 114, 111, 105, 100, 116, 118, 46, 112, 108, 97, 121, 116, 118 ]
[ 18 ] 
[ 146, 3, 15, 8, 9, 16, 10, 26, 7, 84, 80, 77, 49, 55, 49, 69, 32, 1 ]

Send the command

2 messages (and their size) must be sent for each command:

For example, to increase the volume:

[ 6 ]
[ 82, 4, 8, 24, 16, 1 ]
[ 6 ]
[ 82, 4, 8, 24, 16, 2 ]

Ping/Pong

Please, note that the server will send 3 pings and if no pong is received, the connection will be closed.

A ping packet will start with [8,66,6, and you'll have to respond with [74, 2, 8, 25] (no need to send the size).

Start an application

To launch an application:

  • 210,5: the command tag
  • SIZE_WHOLE_MESSAGE
  • 10: tag
  • SIZE_CONTENT
  • CONTENT

With CONTENT that is a deeplink (the deeplink should appear in the AndroidManifest.xml with the value android:host).

Example to launch Netflix, we use the deeplink https://www.netflix.com/title.*:

  • [ 36 ] 
  • [ 210, 5, 33, 10, 31, 104, 116, 116, 112, 115, 58, 47, 47, 119, 119, 119, 46, 110, 101, 116, 102, 108, 105, 120, 46, 99, 111, 109, 47, 116, 105, 116, 108, 101, 46, 42 ]
Clone this wiki locally