Skip to content

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

Aymeric edited this page Sep 25, 2023 · 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 4 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 four 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

Don't forget to send the size of the message first (42), before sending the payload.

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

  • 42: the size of the message
  • 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

As soon as you're connected to the server, it will send you a message that contains some information about the server.

The received message should look like the below:

  • 10: tag
  • SIZE_OF_THE_WHOLE_MESSAGE
  • 8, 255, 4, 18 or 8, 239, 4, 18: ??
  • SIZE_OF_THE_SUB_MESSAGE
  • 10: tag
  • SIZE_OF_MODEL_NAME
  • MODEL_NAME_OF_TV
  • 18: tag
  • SIZE_OF_VENDOR_NAME
  • VENDOR_NAME
  • 24, 1, 34: ??
  • SIZE_OF_SOMETHING_VERSION
  • VERSION_NUMBER
  • 42: tag
  • SIZE_OF_PACKAGE_NAME
  • APP_NAME
  • SIZE_OF_APP_VERSION
  • APP_VERSION

You now have to send a configuration message:

  • 10: tag
  • SIZE_OF_THE_WHOLE_MESSAGE
  • 8, 238, 4, 18
  • SIZE_OF_THE_SUB_MESSAGE
  • 24, 1, 34
  • SIZE_OF_YOUR_APP_VERSION
  • YOUR_APP_VERSION_NUMBER: e.g. 1 becomes 49
  • 42: tag
  • SIZE_OF_PACKAGE_NAME
  • 97, 110, 100, 114, 111, 105, 116, 118, 45, 114, 101, 109, 111, 116, 101: that is androidtv-remote
  • 50: tag
  • SIZE_OF_APP_VERSION
  • APP_VERSION: e.g. 1.0.0 becomes 49, 46, 48, 46, 48

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

[ 36 ]
[ 10, 34, 8, 238, 4, 18, 29, 24, 1, 34, 1, 49, 42, 15, 97, 110, 100, 114, 111, 105, 116, 118, 45, 114, 101, 109, 111, 116, 101, 50, 5, 49, 46, 48, 46, 48 ]

The server returns:

[ 5 ]
[ 10, 3, 8, 255, 4 ]
[ 2 ]
[ 18,0 ]

2nd Configuration Message

After the response [18, 0] 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 3 messages (that could arrive in a different order) providing server's info:

[ 5 ]
[ 194, 2, 2, 8, 1 ] // '1' indicates it's powered, and '0' would indicate it's off
[ 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 ] // indicates the current app
[ 18 ] 
[ 146, 3, 15, 8, 9, 16, 10, 26, 7, 84, 80, 77, 49, 55, 49, 69, 32, 1 ] // indicates the player name and the volume level

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 ]

Some commands, like KEYCODE_CHANNEL_UP and KEYCODE_CHANNEL_DOWN will need only one message:

[ 7 ]
[ 82, 5, 8, $code, 1, 16, 3 ] // $code is e.g. 166 for KEYCODE_CHANNEL_UP

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 [66,6, and you may 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