UNPKG

@octalmage/node-appletv

Version:

A Node.js library for communicating with an Apple TV

148 lines (133 loc) 5 kB
import { load } from 'protobufjs'; import * as path from 'path'; import * as ed25519 from 'ed25519'; import * as crypto from 'crypto'; import * as curve25519 from 'curve25519-n2'; import { AppleTV } from './appletv'; import { Credentials } from './credentials'; import { Message } from './message'; import tlv from './util/tlv'; import enc from './util/encryption'; export class Verifier { constructor(public device: AppleTV) { } verify(): Promise<{}> { var verifyPrivate = Buffer.alloc(32); curve25519.makeSecretKey(verifyPrivate); let verifyPublic = curve25519.derivePublicKey(verifyPrivate) let that = this; let tlvData = tlv.encode( tlv.Tag.Sequence, 0x01, tlv.Tag.PublicKey, verifyPublic ); let message = { status: 0, state: 3, isRetrying: true, isUsingSystemPairing: true, pairingData: tlvData }; return that.device .sendMessage('CryptoPairingMessage', 'CryptoPairingMessage', message, false) .then(() => { return that.waitForSequence(0x02); }) .then(message => { let pairingData = message.payload.pairingData; let tlvData = tlv.decode(pairingData); let sessionPublicKey = tlvData[tlv.Tag.PublicKey]; let encryptedData = tlvData[tlv.Tag.EncryptedData]; if (sessionPublicKey.length != 32) { throw new Error(`sessionPublicKey must be 32 bytes (but was ${sessionPublicKey.length})`); } let sharedSecret = curve25519.deriveSharedSecret(verifyPrivate, sessionPublicKey); let encryptionKey = enc.HKDF( "sha512", Buffer.from("Pair-Verify-Encrypt-Salt"), sharedSecret, Buffer.from("Pair-Verify-Encrypt-Info"), 32 ); let cipherText = encryptedData.slice(0, -16); let hmac = encryptedData.slice(-16); let decryptedData = enc.verifyAndDecrypt(cipherText, hmac, null, Buffer.from('PV-Msg02'), encryptionKey); let innerTLV = tlv.decode(decryptedData); let identifier = innerTLV[tlv.Tag.Username]; let signature = innerTLV[tlv.Tag.Signature]; if (!identifier.equals(that.device.credentials.identifier)) { throw new Error("Identifier mismatch"); } let deviceInfo = Buffer.concat([sessionPublicKey, Buffer.from(identifier), verifyPublic]); if (!ed25519.Verify(deviceInfo, signature, that.device.credentials.publicKey)) { throw new Error("Signature verification failed"); } let material = Buffer.concat([verifyPublic, Buffer.from(that.device.credentials.pairingId), sessionPublicKey]); let keyPair = ed25519.MakeKeypair(that.device.credentials.encryptionKey); let signed = ed25519.Sign(material, keyPair); let plainTLV = tlv.encode( tlv.Tag.Username, Buffer.from(that.device.credentials.pairingId), tlv.Tag.Signature, signed ); let encryptedTLV = Buffer.concat(enc.encryptAndSeal(plainTLV, null, Buffer.from('PV-Msg03'), encryptionKey)); let outerTLV = tlv.encode( tlv.Tag.Sequence, 0x03, tlv.Tag.EncryptedData, encryptedTLV ); let newMessage = { status: 0, state: 3, isRetrying: false, isUsingSystemPairing: true, pairingData: outerTLV }; return that.device .sendMessage('CryptoPairingMessage', 'CryptoPairingMessage', newMessage, false) .then(() => { return that.waitForSequence(0x04); }) .then(() => { let readKey = enc.HKDF( "sha512", Buffer.from("MediaRemote-Salt"), sharedSecret, Buffer.from("MediaRemote-Read-Encryption-Key"), 32 ); let writeKey = enc.HKDF( "sha512", Buffer.from("MediaRemote-Salt"), sharedSecret, Buffer.from("MediaRemote-Write-Encryption-Key"), 32 ); return { readKey: readKey, writeKey: writeKey }; }); }); } private waitForSequence(sequence: number, timeout: number = 3): Promise<Message> { let that = this; let handler = (message: Message, resolve: any) => { let tlvData = tlv.decode(message.payload.pairingData); if (Buffer.from([sequence]).equals(tlvData[tlv.Tag.Sequence])) { resolve(message); } }; return new Promise<Message>((resolve, reject) => { that.device.on('message', (message: Message) => { if (message.type == Message.Type.CryptoPairingMessage) { handler(message, resolve); } }); setTimeout(() => { reject(new Error("Timed out waiting for crypto sequence " + sequence)); }, timeout * 1000); }) .then(value => { that.device.removeListener('message', handler); return value; }); } }