diff --git a/LICENSE b/LICENSE index 971b8e04..12c9d545 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,8 @@ MIT License Copyright (c) 2018 Karl Entwistle +Copyright (c) 2017 Maxim Kulkin +Copyright (c) 2020 Philipp Erbelding Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/lib/ruby_home/accessory_info.rb b/lib/ruby_home/accessory_info.rb index 917526e6..f614ce90 100644 --- a/lib/ruby_home/accessory_info.rb +++ b/lib/ruby_home/accessory_info.rb @@ -16,11 +16,12 @@ def self.reload USERNAME = -'Pair-Setup' - def initialize(device_id: nil, paired_clients: [], password: nil, signature_key: nil) + def initialize(device_id: nil, paired_clients: [], password: nil, signature_key: nil, setup_id: nil) @device_id = device_id @paired_clients = paired_clients @password = password @signature_key = signature_key + @setup_id = setup_id end def username @@ -62,6 +63,10 @@ def signing_key @signing_key ||= RbNaCl::Signatures::Ed25519::SigningKey.new([signature_key].pack('H*')) end + def setup_id + @setup_id ||= SetupID.generate + end + private def signature_key @@ -73,7 +78,8 @@ def persisted_attributes device_id: device_id, paired_clients: paired_clients, password: password, - signature_key: signature_key + signature_key: signature_key, + setup_id: setup_id } end end diff --git a/lib/ruby_home/dns/service.rb b/lib/ruby_home/dns/service.rb index e6893a67..b835d7c2 100644 --- a/lib/ruby_home/dns/service.rb +++ b/lib/ruby_home/dns/service.rb @@ -15,6 +15,10 @@ def register dnssd_service end + def text_record + TextRecord.new(accessory_info: accessory_info, configuration: configuration) + end + private attr_reader :configuration @@ -43,10 +47,6 @@ def host configuration.host end - def text_record - TextRecord.new(accessory_info: accessory_info, configuration: configuration) - end - def accessory_info AccessoryInfo.instance end diff --git a/lib/ruby_home/dns/text_record.rb b/lib/ruby_home/dns/text_record.rb index a0d80f69..04aa3afc 100644 --- a/lib/ruby_home/dns/text_record.rb +++ b/lib/ruby_home/dns/text_record.rb @@ -1,3 +1,5 @@ +require 'digest' +require 'base64' module RubyHome class TextRecord < DNSSD::TextRecord def initialize(accessory_info:, configuration:) @@ -6,6 +8,32 @@ def initialize(accessory_info:, configuration:) super(to_hash) end + # Accessory Category Identifier. Required. Indicates the category that best + # describes the primary function of the accessory. This must have a range of + # 1-65535. + + def accessory_category_identifier + 2 + end + + # Protocol version string . (e.g. "1.0"). Required if value is + # not "1.0". The client should check this before displaying an accessory to + # the user. If the major version is greater than the major version the client + # software was built to support, it should hide the accessory from the user. A + # change in the minor version indicates the protocol is still compatible. This + # mechanism allows future versions of the protocol to hide itself from older + # clients that may not know how to handle it. + + def protocol_version + '1.1' + end + + # Feature flags (e.g. "0x3" for bits 0 and 1). Required if non-zero. + + def feature_flags + 0 + end + private attr_reader :accessory_info, :configuration @@ -19,7 +47,8 @@ def to_hash 'md' => model_name, 'pv' => protocol_version, 's#' => current_state_number, - 'sf' => status_flags + 'sf' => status_flags, + 'sh' => setup_hash } end @@ -33,20 +62,6 @@ def current_configuration_number 1 end - # Accessory Category Identifier. Required. Indicates the category that best - # describes the primary function of the accessory. This must have a range of - # 1-65535. - - def accessory_category_identifier - 2 - end - - # Feature flags (e.g. "0x3" for bits 0 and 1). Required if non-zero. - - def feature_flags - 0 - end - # Status flags (e.g. "0x04" for bit 3). Value should be an unsigned integer. # Required if non-zero. @@ -78,22 +93,27 @@ def model_name configuration.model_name end - # Protocol version string . (e.g. "1.0"). Required if value is - # not "1.0". The client should check this before displaying an accessory to - # the user. If the major version is greater than the major version the client - # software was built to support, it should hide the accessory from the user. A - # change in the minor version indicates the protocol is still compatible. This - # mechanism allows future versions of the protocol to hide itself from older - # clients that may not know how to handle it. - - def protocol_version - 1.0 - end - # Current state number. Required. This must have a value of "1". def current_state_number 1 end + + # Setup id + + def setup_id + accessory_info.setup_id + end + + def setup_hash + # concat setup_id and whatever accessory_info.username is (id field in the record) + input = setup_id + device_id + # sha512 digest that + hashvalue = Digest::SHA2.new(512).digest(input) + # first 4 bytes of that + payload = hashvalue.slice(0,4) + # bas64 to string that + Base64.strict_encode64(payload) + end end end diff --git a/lib/ruby_home/greeter.rb b/lib/ruby_home/greeter.rb index 5c22292a..423c6439 100644 --- a/lib/ruby_home/greeter.rb +++ b/lib/ruby_home/greeter.rb @@ -1,3 +1,4 @@ +require 'rqrcode' module RubyHome module Greeter class << self @@ -9,6 +10,7 @@ def run puts " │ " + pin + " │ " puts " └────────────┘ " puts " " + puts ascii_qrcode end end @@ -23,6 +25,41 @@ def paired? def accessory_info AccessoryInfo.instance end + + def url + record = RubyHome.dns_service.text_record + category = record.accessory_category_identifier + password = accessory_info.password + setup_id = accessory_info.setup_id + + version = 0 + reserved = 0 + flags = record.feature_flags + + payload = 0 + payload |= (version & 0x7) + + payload <<= 4 + payload |= (reserved & 0xf) + + payload <<= 8 + payload |= (category & 0xff) + + payload <<= 4 + payload |= (flags & 0xf) + + payload <<= 27 + payload |= password.gsub('-', '').to_i(10) & 0x7fffffff + + "X-HM://#{payload.to_s(36).rjust(9, '0').upcase}#{setup_id.upcase}" + end + + def ascii_qrcode + qrcode = RQRCode::QRCode.new(url) + terminal_qr = qrcode.to_s + terminal_qr.gsub!(' ', '⬜') + terminal_qr.gsub!('x', '⬛') + end end end end diff --git a/lib/ruby_home/service.rb b/lib/ruby_home/service.rb index 95a71567..0637a009 100644 --- a/lib/ruby_home/service.rb +++ b/lib/ruby_home/service.rb @@ -44,5 +44,9 @@ def characteristic(characteristic_name) characteristic.name == characteristic_name end end + + def to_s + "#{@name} - #{@description}" + end end end diff --git a/lib/ruby_home/setup_id.rb b/lib/ruby_home/setup_id.rb new file mode 100644 index 00000000..22ce968b --- /dev/null +++ b/lib/ruby_home/setup_id.rb @@ -0,0 +1,21 @@ +module RubyHome + module SetupID + class << self + # Requirements for Setup ID of the accessory are not completely clear. + # A unique random string of letters, generated at every + # factory reset and persisted across reboots seems fine. + + def generate(count=4) + id = '' + count.times { + id << random_letter + } + id + end + + def random_letter + ('A'..'Z').to_a[rand(26)] + end + end + end +end diff --git a/lib/ruby_home/version.rb b/lib/ruby_home/version.rb index 72053102..03ceae4b 100644 --- a/lib/ruby_home/version.rb +++ b/lib/ruby_home/version.rb @@ -1,3 +1,3 @@ module RubyHome - VERSION = '0.2.4' + VERSION = '0.3.0' end diff --git a/rubyhome.gemspec b/rubyhome.gemspec index 835d6b73..232e4dc3 100644 --- a/rubyhome.gemspec +++ b/rubyhome.gemspec @@ -31,9 +31,11 @@ Gem::Specification.new do |spec| spec.add_dependency 'hkdf', '~> 0.3.0' spec.add_dependency 'oj', '~> 3.10' spec.add_dependency 'rbnacl', '~> 7.0' + spec.add_dependency 'rqrcode', '~> 1.2.0' spec.add_dependency 'ruby_home-srp', '~> 1.3' spec.add_dependency 'ruby_home-tlv', '~> 0.1' spec.add_dependency 'sinatra', '~> 2.0' + spec.add_dependency 'webrick', '~> 1.7.0' spec.add_dependency 'wisper', '~> 2.0' spec.add_development_dependency 'byebug', '~> 11.0' diff --git a/spec/lib/setup_id_spec.rb b/spec/lib/setup_id_spec.rb new file mode 100644 index 00000000..9fc602f6 --- /dev/null +++ b/spec/lib/setup_id_spec.rb @@ -0,0 +1,9 @@ +RSpec.describe RubyHome::SetupID do + + describe 'Setup ID generator' do + it 'outputs lenght of 4' do + expect(RubyHome::SetupID.generate.length).to eql(4) + end + end + +end