-
-
Notifications
You must be signed in to change notification settings - Fork 14
Google TV (aka Android TV) Remote Control (v2)
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.
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
- if you get a negative number, then you must convert the unsigned decimal to a signed one, using https://onlinetoolz.net/unsigned-signed#base=10&value=-14&bits=8. For example,
-56
is actually200
. - you can use https://www.rapidtables.com/convert/number/ascii-hex-bin-dec-converter.html to convert between decimal and ascii.
- you can find a JavaScript implementation here: https://github.com/louis49/androidtv-remote
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();
?>
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 codeOK
-
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]
, with21
the size, and105,110,102,111,46,107,111,100,111,110,111,46,97,115,115,105,115,116,97,110,116
forinfo.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]
, with13
the size, and105, 110, 116, 101, 114, 102, 97, 99, 101, 32, 119, 101, 98
forinterface 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 codeOK
(it could be16, 144, 3
forERROR
, or16, 145, 3
forBAD_CONFIGURATION
)
We respond with the below payload:
-
8, 2
: it's the protocol version 2 -
16, 200, 1
: it's the status codeOK
-
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
forENCODING_TYPE_UNKNOWN
,1
forENCODING_TYPE_ALPHANUMERIC
,2
forENCODING_TYPE_NUMERIC
,3
forENCODING_TYPE_HEXADECIMAL
,4
forENCODING_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
forROLE_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 ]
We respond with the below payload:
-
8, 2
: it's the protocol version 2 -
16, 200, 1
: it's the status codeOK
-
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
forENCODING_TYPE_UNKNOWN
,1
forENCODING_TYPE_ALPHANUMERIC
,2
forENCODING_TYPE_NUMERIC
,3
forENCODING_TYPE_HEXADECIMAL
,4
forENCODING_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
forROLE_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.
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)));
}
We send it with the below payload:
-
8, 2
: it's the protocol version 2 -
16, 200, 1
: it's the status codeOK
-
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 codeOK
- several bytes…
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:
- Send the 1st configuration message
- Send the 2nd configuration message
- Send the command
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
becomes65, 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
becomes75, 111, 100, 111, 110, 111
-
24, 1, 34, 2, 49, 48
: ?? -
42
: tag SIZE_OF_PACKAGE_NAME
-
APP_NAME
: e.g.info.kodono.assistant
becomes105,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
becomes49, 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 ]
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 ]
2 messages (and their size) must be sent for each command:
-
82,4,8
: the command tag -
KEY_EVENT
: the constant value from https://developer.android.com/reference/android/view/KeyEvent (e.g.24
forKEYCODE_VOLUME_UP
) -
16, 1
forpress
or16, 2
for release
For example, to increase the volume:
[ 6 ]
[ 82, 4, 8, 24, 16, 1 ]
[ 6 ]
[ 82, 4, 8, 24, 16, 2 ]
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).
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 ]