evandcoleman/node-appletv

View on GitHub
src/lib/verifier.ts

Summary

Maintainability
A
3 hrs
Test Coverage
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';

type PairingData = {
  sessionPublicKey: Buffer;
  sharedSecret: Buffer;
  encryptionKey: Buffer;
  pairingData: Buffer;
}

export class Verifier {
  constructor(public device: AppleTV) {

  }

  async verify(): Promise<{}> {
    var verifyPrivate = Buffer.alloc(32);
    curve25519.makeSecretKey(verifyPrivate);
    let verifyPublic = curve25519.derivePublicKey(verifyPrivate);
    let { sessionPublicKey, encryptionKey, sharedSecret, pairingData } = await this.requestPairingData(verifyPublic, verifyPrivate);
    
    let tlvData = tlv.decode(pairingData);
    let identifier = tlvData[tlv.Tag.Username];
    let signature = tlvData[tlv.Tag.Signature];

    if (!identifier.equals(this.device.credentials.identifier)) {
      throw new Error("Identifier mismatch");
    }

    let deviceInfo = Buffer.concat([sessionPublicKey, Buffer.from(identifier), verifyPublic]);
    if (!ed25519.Verify(deviceInfo, signature, this.device.credentials.publicKey)) {
      throw new Error("Signature verification failed");
    }

    return await this.completeVerification(verifyPublic, sessionPublicKey, encryptionKey, sharedSecret);
  }

  private async requestPairingData(verifyPublic: Buffer, verifyPrivate: Buffer): Promise<PairingData> {
    let encodedData = tlv.encode(
      tlv.Tag.Sequence, 0x01,
      tlv.Tag.PublicKey, verifyPublic
    );
    let message = {
      status: 0,
      state: 3,
      isRetrying: true,
      isUsingSystemPairing: true,
      pairingData: encodedData
    };

    await this.device.sendMessage('CryptoPairingMessage', 'CryptoPairingMessage', message, false);
    let pairingDataResponse = await this.device.waitForSequence(0x02);
    let pairingData = pairingDataResponse.payload.pairingData;
    let decodedData = tlv.decode(pairingData);
    let sessionPublicKey = decodedData[tlv.Tag.PublicKey];
    let encryptedData = decodedData[tlv.Tag.EncryptedData];

    if (sessionPublicKey.length != 32) {
      throw new Error(`sessionPublicKey must be 32 bytes (but was ${sessionPublicKey.length})`);
    }

    let cipherText = encryptedData.slice(0, -16);
    let hmac = encryptedData.slice(-16);
    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 decryptedData = enc.verifyAndDecrypt(cipherText, hmac, null, Buffer.from('PV-Msg02'), encryptionKey);

    return {
      sessionPublicKey: sessionPublicKey,
      sharedSecret: sharedSecret,
      encryptionKey: encryptionKey,
      pairingData: decryptedData
    };
  }

  private async completeVerification(verifyPublic: Buffer, sessionPublicKey: Buffer, encryptionKey: Buffer, sharedSecret: Buffer): Promise<{}> {
    let material = Buffer.concat([verifyPublic, Buffer.from(this.device.credentials.pairingId), sessionPublicKey]);
    let keyPair = ed25519.MakeKeypair(this.device.credentials.encryptionKey);
    let signed = ed25519.Sign(material, keyPair);
    let plainTLV = tlv.encode(
      tlv.Tag.Username, Buffer.from(this.device.credentials.pairingId),
      tlv.Tag.Signature, signed
    );
    let encryptedTLV = Buffer.concat(enc.encryptAndSeal(plainTLV, null, Buffer.from('PV-Msg03'), encryptionKey));
    let tlvData = tlv.encode(
      tlv.Tag.Sequence, 0x03,
      tlv.Tag.EncryptedData, encryptedTLV
    );
    let message = {
      status: 0,
      state: 3,
      isRetrying: false,
      isUsingSystemPairing: true,
      pairingData: tlvData
    };

    await this.device.sendMessage('CryptoPairingMessage', 'CryptoPairingMessage', message, false);
    await this.device.waitForSequence(0x04);
    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
    };
  }
}