module HAP::Pairing

Constants

ERROR_NAMES
ERROR_TYPES

Public Instance Methods

pair_setup(password, &block) click to toggle source
# File lib/hap_client/pairing.rb, line 27
def pair_setup(password, &block)
  info("Pair Setup Step 1/3")
  @mode = :pair_setup
  @password = password
  srp_start_request()

  if block_given?
    @pair_setup_callback = block
  end
end
pair_verify(&block) click to toggle source
# File lib/hap_client/pairing.rb, line 38
def pair_verify(&block)
  info("Pair Verify 1/2")
  @mode = :pair_verify
  verify_start_request()

  if block_given?
    @pair_verify_callback = block
  end
end

Private Instance Methods

bin_to_hex(s) click to toggle source
# File lib/hap_client/pairing.rb, line 371
def bin_to_hex(s)
  s.unpack('H*')[0]
end
call_pair_setup_callback(status, data=nil) click to toggle source
# File lib/hap_client/pairing.rb, line 77
def call_pair_setup_callback(status, data=nil)
  if @pair_setup_callback
    t = @pair_setup_callback
    @pair_setup_callback = nil
    t.call(status, data)
  end
end
call_pair_verify_callback(status, data=nil) click to toggle source
# File lib/hap_client/pairing.rb, line 242
def call_pair_verify_callback(status, data=nil)
  if @pair_verify_callback
    t = @pair_verify_callback
    @pair_verify_callback = nil
    t.call(status, data)
  end
end
check_tlv_response(data) click to toggle source
# File lib/hap_client/pairing.rb, line 358
def check_tlv_response(data)
  data = RubyHome::HAP::TLV.read(data)

  debug("Response: " + data.to_s)

  if data['kTLVType_Error']
    error("Failed to pair: #{data}")
    raise PairingError, ERROR_NAMES[data['kTLVType_Error']]
  end

  return data
end
get_pairing_context() click to toggle source
# File lib/hap_client/pairing.rb, line 341
def get_pairing_context()
  {
    'client_id' => @client_id,
    'signature_key' => @signature_key,
    'accessoryltpk' => @accessoryltpk.unpack1('H*')
  }
end
hex_to_bin(s) click to toggle source
# File lib/hap_client/pairing.rb, line 375
def hex_to_bin(s)
  s.scan(/../).map { |x| x.hex.chr }.join
end
pair_setup_parse(data) click to toggle source
# File lib/hap_client/pairing.rb, line 50
def pair_setup_parse(data)
  begin
    response = check_tlv_response(data)

    case response['kTLVType_State']
    when 2
      info("Pair Setup Step 2/3")
      srp_verify_request(response, @password)
    when 4
      srp_verify(response)

      info("Pair Setup Step 3/3")
      srp_exchange_request()
    when 6
      info("Verifying Server Exchange")
      srp_exchange_verify(response)

      call_pair_setup_callback(true)
    else
      error("Unknown Pair Setup State: #{response['kTLVType_State']}")
    end
  rescue PairingError => e
    error("Pair Setup Error: #{e}")
    call_pair_setup_callback(false, e.to_s)
  end
end
pair_verify_parse(data) click to toggle source
# File lib/hap_client/pairing.rb, line 220
def pair_verify_parse(data)
  begin
    response = check_tlv_response(data)

    case response['kTLVType_State']
    when 2
      info("Pair Verify 2/2")
      verify_finish_request(response)
    when 4
      verify_finish_verify()
      @mode = :paired

      call_pair_verify_callback(true)
    else
      error("Unknown Pair Verify State: #{response['kTLVType_State']}")
    end
  rescue PairingError => e
    error("Pair Verify Error: #{e}")
    call_pair_verify_callback(false, e.to_s)
  end
end
set_pairing_context(context) click to toggle source
# File lib/hap_client/pairing.rb, line 349
def set_pairing_context(context)
  context = JSON.parse(context) if context.is_a?(String)
  @client_id = context['client_id']
  @signature_key = context['signature_key']
  @accessoryltpk = hex_to_bin(context['accessoryltpk'])

  @signing_key = Ed25519::SigningKey.new([@signature_key].pack('H*'))
end
srp_exchange_request() click to toggle source
# File lib/hap_client/pairing.rb, line 136
def srp_exchange_request()
  debug("Pair Setup SRP Exchange Request")

  debug("Generate Longterm key")
  @signature_key = Ed25519::SigningKey.generate.to_bytes.unpack1('H*')
  @signing_key = Ed25519::SigningKey.new([@signature_key].pack('H*'))

  debug("Generating device id")
  @client_id = RubyHome::DeviceID.generate()

  debug("Generating Encryption key")
  hkdf = RubyHome::HAP::Crypto::HKDF.new(info: 'Pair-Setup-Encrypt-Info', salt: 'Pair-Setup-Encrypt-Salt')
  key = hkdf.encrypt(@srp_session_key)
  @chacha20poly1305ietf = RubyHome::HAP::Crypto::ChaCha20Poly1305.new(key)

  debug("Generating ClientX")
  hkdf = RubyHome::HAP::Crypto::HKDF.new(info: 'Pair-Setup-Controller-Sign-Info', salt: 'Pair-Setup-Controller-Sign-Salt')
  clientX = hkdf.encrypt(@srp_session_key)

  debug("Generating ClientInfo")
  clientLTPK = @signing_key.verify_key.to_bytes
  clientInfo = [
    clientX.unpack1('H*'),
    @client_id.unpack1('H*'),
    clientLTPK.unpack1('H*')
  ].join

  debug("Generating Client Signature")
  clientSignature = @signing_key.sign([clientInfo].pack('H*'))

  debug("Generating Encrypted Data")
  subtlv = RubyHome::HAP::TLV.encode({
                                       'kTLVType_Identifier' => @client_id,
                                       'kTLVType_PublicKey' => clientLTPK,
                                       'kTLVType_Signature' => clientSignature
                                     })
  nonce = RubyHome::HAP::HexPad.pad('PS-Msg05')
  encrypted_data = @chacha20poly1305ietf.encrypt(nonce, subtlv)

  debug("Sending Encrypted Request to Server")
  data = RubyHome::HAP::TLV.encode({
                                     'kTLVType_State' => 5,
                                     'kTLVType_EncryptedData' => encrypted_data
                                   })
  post("/pair-setup", "application/pairing+tlv8", data)
end
srp_exchange_verify(response) click to toggle source
# File lib/hap_client/pairing.rb, line 183
def srp_exchange_verify(response)
  debug("Decrypting Server Response")
  encrypted_data = response['kTLVType_EncryptedData']
  nonce = RubyHome::HAP::HexPad.pad('PS-Msg06')

  decrypted_data = @chacha20poly1305ietf.decrypt(nonce, encrypted_data)
  unpacked_decrypted_data = RubyHome::HAP::TLV.read(decrypted_data)
  @chacha20poly1305ietf = nil

  debug("Verifying Server Signature")
  @serverPairingId = unpacked_decrypted_data['kTLVType_Identifier']
  serverSignature = unpacked_decrypted_data['kTLVType_Signature']
  @accessoryltpk = unpacked_decrypted_data['kTLVType_PublicKey']

  hkdf = RubyHome::HAP::Crypto::HKDF.new(info: 'Pair-Setup-Accessory-Sign-Info', salt: 'Pair-Setup-Accessory-Sign-Salt')
  accessoryx = hkdf.encrypt(@srp_session_key)

  accessoryinfo = [
    accessoryx.unpack1('H*'),
    @serverPairingId.unpack1('H*'),
    @accessoryltpk.unpack1('H*')
  ].join
  verify_key = RbNaCl::Signatures::Ed25519::VerifyKey.new(@accessoryltpk)

  begin
    if verify_key.verify(serverSignature, [accessoryinfo].pack('H*'))
      info("Pairing Success! Server Pairing ID: #{@serverPairingId}")
    else
      error("Failed to verify Server Signature")
      raise PairingError, "Failed to verify Server Signature"
    end
  rescue RbNaCl::BadSignatureError
    error("Failed to verify Server Signature")
    raise PairingError, "Failed to verify Server Signature"
  end
end
srp_start_request() click to toggle source
# File lib/hap_client/pairing.rb, line 85
def srp_start_request()
  debug("Pair Setup SRP Start Request")
  data = RubyHome::HAP::TLV.encode({
                                     'kTLVType_State' => 0x01,
                                     'kTLVType_Method' => 0x00
                                   })
  post("/pair-setup", "application/pairing+tlv8", data)
end
srp_verify(response) click to toggle source
# File lib/hap_client/pairing.rb, line 125
def srp_verify(response)
  debug("Verifying Server Proof")
  serverProof = bin_to_hex(response['kTLVType_Proof'])

  unless @srp_client.verify(serverProof)
    raise PairingError, "Failed to verify server proof"
  end

  @srp_client = nil
end
srp_verify_request(response, password) click to toggle source
# File lib/hap_client/pairing.rb, line 94
def srp_verify_request(response, password)
  debug("Pair Setup SRP Verify Request")

  username = 'Pair-Setup'
  debug("Using #{password} to pair with device")

  # convert bin variables to hex strings
  salt = bin_to_hex(response["kTLVType_Salt"])
  serverPublicKey = bin_to_hex(response["kTLVType_PublicKey"])

  debug("Generating Client Public/Private Keys")
  @srp_client = RubyHome::SRP::Client.new(3072)
  clientPublicKey = hex_to_bin(@srp_client.start_authentication())

  debug("Process Challenge from Server")
  client_M = hex_to_bin(@srp_client.process_challenge(username, password, salt, serverPublicKey))

  debug("Send Client Proof to Server")
  data = RubyHome::HAP::TLV.encode({
                                     'kTLVType_Proof' => client_M,
                                     'kTLVType_PublicKey' => clientPublicKey,
                                     'kTLVType_State' => 3,
                                     'kTLVType_Method' => 0
                                   })

  # Save session key
  @srp_session_key = @srp_client.K

  post("/pair-setup", "application/pairing+tlv8", data)
end
verify_finish_request(response) click to toggle source
# File lib/hap_client/pairing.rb, line 263
def verify_finish_request(response)
  debug("Generating shared secret")
  server_public_key = X25519::MontgomeryU.new(response['kTLVType_PublicKey'])
  @shared_secret = @client_secret_key.multiply(server_public_key).to_bytes

  debug("Generating session key")
  hkdf = RubyHome::HAP::Crypto::HKDF.new(info: 'Pair-Verify-Encrypt-Info', salt: 'Pair-Verify-Encrypt-Salt')
  session_key = hkdf.encrypt(@shared_secret)

  debug("Decrypting data")
  subtlv = response['kTLVType_EncryptedData']
  chacha20poly1305ietf = RubyHome::HAP::Crypto::ChaCha20Poly1305.new(session_key)
  nonce = RubyHome::HAP::HexPad.pad('PV-Msg02')
  decrypted_data = chacha20poly1305ietf.decrypt(nonce, subtlv)
  decrypted_data = RubyHome::HAP::TLV.read(decrypted_data)

  debug("Verifying Server Signature")
  server_device_id = decrypted_data['kTLVType_Identifier']
  serverSignature = decrypted_data['kTLVType_Signature']

  accessoryinfo = [
    server_public_key.to_bytes.unpack1('H*'),
    server_device_id.unpack1('H*'),
    @client_public_key.unpack1('H*')
  ].join
  verify_key = RbNaCl::Signatures::Ed25519::VerifyKey.new(@accessoryltpk)

  begin
    if !verify_key.verify(serverSignature, [accessoryinfo].pack('H*'))
      error("Server signature INVALID!")
      raise PairingError, "Server signature INVALID!"
    end
  rescue RbNaCl::BadSignatureError
    error("Server signature INVALID!")
    raise PairingError, "Server signature INVALID!"
  end

  debug("Generating Client Info")
  clientInfo = [
    @client_public_key.unpack1('H*'),
    @client_id.unpack1('H*'),
    server_public_key.to_bytes.unpack1('H*')
  ].join

  debug("Generating Client Signature")
  clientSignature = @signing_key.sign([clientInfo].pack('H*'))

  debug("Generating Encrypted Data")
  subtlv = RubyHome::HAP::TLV.encode({
                                       'kTLVType_Identifier' => @client_id,
                                       'kTLVType_Signature' => clientSignature
                                     })

  chacha20poly1305ietf = RubyHome::HAP::Crypto::ChaCha20Poly1305.new(session_key)
  nonce = RubyHome::HAP::HexPad.pad('PV-Msg03')
  encrypted_data = chacha20poly1305ietf.encrypt(nonce, subtlv)

  debug("Sending Encrypted Request to Server")
  data = RubyHome::HAP::TLV.encode({
                                     'kTLVType_State' => 3,
                                     'kTLVType_EncryptedData' => encrypted_data
                                   })

  post("/pair-verify", "application/pairing+tlv8", data)
end
verify_finish_verify() click to toggle source
# File lib/hap_client/pairing.rb, line 329
def verify_finish_verify()
  hkdf = RubyHome::HAP::Crypto::HKDF.new(info: 'Control-Write-Encryption-Key', salt: 'Control-Salt')
  @controller_to_accessory_key = hkdf.encrypt(@shared_secret)

  hkdf = RubyHome::HAP::Crypto::HKDF.new(info: 'Control-Read-Encryption-Key', salt: 'Control-Salt')
  @accessory_to_controller_key = hkdf.encrypt(@shared_secret)

  @shared_secret = nil

  info("Pair Verify Complete")
end
verify_start_request() click to toggle source
# File lib/hap_client/pairing.rb, line 250
def verify_start_request()
  debug("Generating new Session Public/Private Keys")
  @client_secret_key = X25519::Scalar.generate
  @client_public_key = @client_secret_key.public_key.to_bytes

  debug("Sending verify Request to Server")
  data = RubyHome::HAP::TLV.encode({
                                     'kTLVType_State' => 1,
                                     'kTLVType_PublicKey' => @client_public_key
                                   })
  post("/pair-verify", "application/pairing+tlv8", data)
end