ruby_home 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.hound.yml +2 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +9 -0
  6. data/.travis.yml +21 -0
  7. data/Gemfile +6 -0
  8. data/LICENSE +21 -0
  9. data/README.md +36 -0
  10. data/Rakefile +7 -0
  11. data/bin/console +14 -0
  12. data/bin/rubyhome +16 -0
  13. data/bin/setup +8 -0
  14. data/lib/ruby_home.rb +6 -0
  15. data/lib/ruby_home/accessory_info.rb +99 -0
  16. data/lib/ruby_home/broadcast.rb +31 -0
  17. data/lib/ruby_home/config/characteristics.yml +1692 -0
  18. data/lib/ruby_home/config/services.yml +416 -0
  19. data/lib/ruby_home/device_id.rb +30 -0
  20. data/lib/ruby_home/dns/service.rb +44 -0
  21. data/lib/ruby_home/dns/text_record.rb +100 -0
  22. data/lib/ruby_home/factories/accessory_factory.rb +78 -0
  23. data/lib/ruby_home/factories/characteristic_factory.rb +57 -0
  24. data/lib/ruby_home/factories/templates/characteristic_template.rb +43 -0
  25. data/lib/ruby_home/factories/templates/service_template.rb +50 -0
  26. data/lib/ruby_home/hap/accessory.rb +26 -0
  27. data/lib/ruby_home/hap/characteristic.rb +60 -0
  28. data/lib/ruby_home/hap/hex_pad.rb +13 -0
  29. data/lib/ruby_home/hap/hkdf_encryption.rb +34 -0
  30. data/lib/ruby_home/hap/http_decryption.rb +58 -0
  31. data/lib/ruby_home/hap/http_encryption.rb +43 -0
  32. data/lib/ruby_home/hap/service.rb +38 -0
  33. data/lib/ruby_home/http/application.rb +28 -0
  34. data/lib/ruby_home/http/cache.rb +30 -0
  35. data/lib/ruby_home/http/controllers/accessories_controller.rb +19 -0
  36. data/lib/ruby_home/http/controllers/application_controller.rb +37 -0
  37. data/lib/ruby_home/http/controllers/characteristics_controller.rb +49 -0
  38. data/lib/ruby_home/http/controllers/pair_setups_controller.rb +146 -0
  39. data/lib/ruby_home/http/controllers/pair_verifies_controller.rb +81 -0
  40. data/lib/ruby_home/http/controllers/pairings_controller.rb +38 -0
  41. data/lib/ruby_home/http/hap_request.rb +56 -0
  42. data/lib/ruby_home/http/hap_response.rb +57 -0
  43. data/lib/ruby_home/http/hap_server.rb +65 -0
  44. data/lib/ruby_home/http/serializers/accessory_serializer.rb +21 -0
  45. data/lib/ruby_home/http/serializers/characteristic_serializer.rb +26 -0
  46. data/lib/ruby_home/http/serializers/characteristic_value_serializer.rb +21 -0
  47. data/lib/ruby_home/http/serializers/object_serializer.rb +39 -0
  48. data/lib/ruby_home/http/serializers/service_serializer.rb +20 -0
  49. data/lib/ruby_home/identifier_cache.rb +59 -0
  50. data/lib/ruby_home/rack/handler/hap_server.rb +26 -0
  51. data/lib/ruby_home/tlv.rb +83 -0
  52. data/lib/ruby_home/tlv/bytes.rb +19 -0
  53. data/lib/ruby_home/tlv/int.rb +15 -0
  54. data/lib/ruby_home/tlv/utf8.rb +18 -0
  55. data/lib/ruby_home/version.rb +3 -0
  56. data/rubyhome.gemspec +43 -0
  57. data/sbin/characteristic_generator.rb +83 -0
  58. data/sbin/service_generator.rb +69 -0
  59. metadata +352 -0
@@ -0,0 +1,81 @@
1
+ require 'x25519'
2
+ require_relative '../../hap/hex_pad'
3
+ require_relative '../../hap/hkdf_encryption'
4
+ require_relative '../../tlv'
5
+ require_relative 'application_controller'
6
+
7
+ module RubyHome
8
+ module HTTP
9
+ class PairVerifiesController < ApplicationController
10
+ post '/pair-verify' do
11
+ content_type 'application/pairing+tlv8'
12
+
13
+ case unpack_request['kTLVType_State']
14
+ when 1
15
+ verify_start_response
16
+ when 3
17
+ verify_finish_response
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def verify_start_response
24
+ secret_key = X25519::Scalar.generate
25
+ public_key = secret_key.public_key.to_bytes.unpack('H*')[0]
26
+ client_public_key = X25519::MontgomeryU.new([unpack_request['kTLVType_PublicKey']].pack('H*'))
27
+ shared_secret = secret_key.multiply(client_public_key).to_bytes
28
+ cache[:shared_secret] = shared_secret
29
+
30
+ accessoryinfo = [
31
+ public_key,
32
+ TLV::Utf8.pack(accessory_info.device_id),
33
+ client_public_key.to_bytes.unpack('H*')[0]
34
+ ].join
35
+
36
+ signing_key = accessory_info.signing_key
37
+ accessorysignature = signing_key.sign([accessoryinfo].pack('H*')).unpack('H*')[0]
38
+
39
+ subtlv = TLV.pack({
40
+ 'kTLVType_Identifier' => accessory_info.device_id,
41
+ 'kTLVType_Signature' => accessorysignature
42
+ })
43
+
44
+ hkdf = HAP::HKDFEncryption.new(info: 'Pair-Verify-Encrypt-Info', salt: 'Pair-Verify-Encrypt-Salt')
45
+ session_key = hkdf.encrypt(shared_secret)
46
+ cache[:session_key] = session_key
47
+
48
+ chacha20poly1305ietf = RbNaCl::AEAD::ChaCha20Poly1305IETF.new(session_key)
49
+ nonce = HAP::HexPad.pad('PV-Msg02')
50
+ encrypted_data = chacha20poly1305ietf.encrypt(nonce, subtlv, nil).unpack('H*')[0]
51
+
52
+ TLV.pack({
53
+ 'kTLVType_State' => 2,
54
+ 'kTLVType_PublicKey' => public_key,
55
+ 'kTLVType_EncryptedData' => encrypted_data
56
+ })
57
+ end
58
+
59
+ def verify_finish_response
60
+ encrypted_data = unpack_request['kTLVType_EncryptedData']
61
+
62
+ chacha20poly1305ietf = RbNaCl::AEAD::ChaCha20Poly1305IETF.new(cache[:session_key])
63
+ nonce = HAP::HexPad.pad('PV-Msg03')
64
+ decrypted_data = chacha20poly1305ietf.decrypt(nonce, [encrypted_data].pack('H*'), nil)
65
+ unpacked_decrypted_data = TLV.unpack(decrypted_data)
66
+
67
+ if accessory_info.paired_clients.any? {|h| h[:identifier] == unpacked_decrypted_data['kTLVType_Identifier']}
68
+ hkdf = HAP::HKDFEncryption.new(info: 'Control-Write-Encryption-Key', salt: 'Control-Salt')
69
+ cache[:controller_to_accessory_key] = hkdf.encrypt(cache[:shared_secret])
70
+
71
+ hkdf = HAP::HKDFEncryption.new(info: 'Control-Read-Encryption-Key', salt: 'Control-Salt')
72
+ cache[:accessory_to_controller_key] = hkdf.encrypt(cache[:shared_secret])
73
+
74
+ TLV.pack({'kTLVType_State' => 4})
75
+ else
76
+ TLV.pack({'kTLVType_State' => 4, 'kTLVType_Error' => 2})
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,38 @@
1
+ require_relative 'application_controller'
2
+
3
+ module RubyHome
4
+ module HTTP
5
+ class PairingsController < ApplicationController
6
+ post '/pairings' do
7
+ content_type 'application/pairing+tlv8'
8
+
9
+ case unpack_request['kTLVType_Method']
10
+ when 3
11
+ add_pairing
12
+ when 4
13
+ remove_pairing
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def add_pairing
20
+ pairing_params = {
21
+ admin: !!unpack_request['kTLVType_Permissions'],
22
+ identifier: unpack_request['kTLVType_Identifier'],
23
+ public_key: unpack_request['kTLVType_PublicKey']
24
+ }
25
+ accessory_info.add_paired_client pairing_params
26
+
27
+ TLV.pack({'kTLVType_State' => 2})
28
+ end
29
+
30
+ def remove_pairing
31
+ accessory_info.remove_paired_client(unpack_request['kTLVType_Identifier'])
32
+
33
+ response['connection'] = 'close'
34
+ TLV.pack({'kTLVType_State' => 2})
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,56 @@
1
+ require 'webrick/httprequest'
2
+ require_relative '../hap/http_decryption'
3
+
4
+ module RubyHome
5
+ module HTTP
6
+ class HAPRequest < WEBrick::HTTPRequest
7
+ def initialize(*args, request_id: )
8
+ @_request_id = request_id
9
+ cache[:controller_to_accessory_count] ||= 0
10
+
11
+ super(*args)
12
+ end
13
+
14
+ def parse(socket=nil)
15
+ if decryption_time?
16
+ request_line = socket.read_nonblock(@buffer_size)
17
+
18
+ decrypted_request = decrypter.decrypt(request_line).join
19
+ cache[:controller_to_accessory_count] = decrypter.count
20
+
21
+ super(StringIO.new(decrypted_request))
22
+ else
23
+ super(socket)
24
+ end
25
+ end
26
+
27
+ def received_encrypted_request?
28
+ cache[:controller_to_accessory_count] >= 1
29
+ end
30
+
31
+ private
32
+
33
+ def decrypter
34
+ @_decrypter ||= RubyHome::HAP::HTTPDecryption.new(decryption_key, decrypter_params)
35
+ end
36
+
37
+ def decrypter_params
38
+ {
39
+ count: cache[:controller_to_accessory_count]
40
+ }
41
+ end
42
+
43
+ def decryption_time?
44
+ !!decryption_key
45
+ end
46
+
47
+ def decryption_key
48
+ cache[:controller_to_accessory_key]
49
+ end
50
+
51
+ def cache
52
+ GlobalCache.instance[@_request_id] ||= Cache.new
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,57 @@
1
+ require 'webrick/httpresponse'
2
+ require_relative '../hap/http_encryption'
3
+
4
+ module RubyHome
5
+ module HTTP
6
+ class HAPResponse < WEBrick::HTTPResponse
7
+ def initialize(*args, request_id: )
8
+ @_request_id = request_id
9
+ cache[:accessory_to_controller_count] ||= 0
10
+
11
+ super(*args)
12
+ end
13
+
14
+ def send_response(socket)
15
+ if encryption_time?
16
+ response = String.new
17
+ super(response)
18
+
19
+ encrypted_response = encrypter.encrypt(response).join
20
+ cache[:accessory_to_controller_count] = encrypter.count
21
+
22
+ _write_data(socket, encrypted_response)
23
+ else
24
+ super(socket)
25
+ end
26
+ end
27
+
28
+ attr_writer :received_encrypted_request
29
+
30
+ private
31
+
32
+ def encrypter
33
+ @_encrypter ||= RubyHome::HAP::HTTPEncryption.new(encryption_key, encrypter_params)
34
+ end
35
+
36
+ def encrypter_params
37
+ {
38
+ count: cache[:accessory_to_controller_count]
39
+ }
40
+ end
41
+
42
+ def encryption_time?
43
+ encryption_key && !!@received_encrypted_request
44
+ end
45
+
46
+ def encryption_key
47
+ cache[:accessory_to_controller_key]
48
+ end
49
+
50
+ def cache
51
+ GlobalCache.instance[@_request_id] ||= Cache.new
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+
@@ -0,0 +1,65 @@
1
+ require 'webrick/httpserver'
2
+ require 'webrick/httpstatus'
3
+ require_relative 'hap_request'
4
+ require_relative 'hap_response'
5
+
6
+ module RubyHome
7
+ module HTTP
8
+ class HAPServer < WEBrick::HTTPServer
9
+ def run(sock)
10
+ while true
11
+ res = RubyHome::HTTP::HAPResponse.new(@config, request_id: sock.object_id)
12
+ req = RubyHome::HTTP::HAPRequest.new(@config, request_id: sock.object_id)
13
+ server = self
14
+ begin
15
+ timeout = @config[:RequestTimeout]
16
+ while timeout > 0
17
+ break if sock.to_io.wait_readable(0.5)
18
+ break if @status != :Running
19
+ timeout -= 0.5
20
+ end
21
+ raise WEBrick::HTTPStatus::EOFError if timeout <= 0 || @status != :Running
22
+ raise WEBrick::HTTPStatus::EOFError if sock.eof?
23
+ req.parse(sock)
24
+ res.received_encrypted_request = req.received_encrypted_request?
25
+ res.request_method = req.request_method
26
+ res.request_uri = req.request_uri
27
+ res.request_http_version = req.http_version
28
+ res.keep_alive = req.keep_alive?
29
+ server = lookup_server(req) || self
30
+ if callback = server[:RequestCallback]
31
+ callback.call(req, res)
32
+ elsif callback = server[:RequestHandler]
33
+ msg = ':RequestHandler is deprecated, please use :RequestCallback'
34
+ @logger.warn(msg)
35
+ callback.call(req, res)
36
+ end
37
+ server.service(req, res)
38
+ rescue WEBrick::HTTPStatus::EOFError, WEBrick::HTTPStatus::RequestTimeout => ex
39
+ res.set_error(ex)
40
+ rescue WEBrick::HTTPStatus::Error => ex
41
+ @logger.error(ex.message)
42
+ res.set_error(ex)
43
+ rescue WEBrick::HTTPStatus::Status => ex
44
+ res.status = ex.code
45
+ rescue StandardError => ex
46
+ @logger.error(ex)
47
+ res.set_error(ex, true)
48
+ ensure
49
+ if req.request_line
50
+ if req.keep_alive? && res.keep_alive?
51
+ req.fixup()
52
+ end
53
+ res.send_response(sock)
54
+ server.access_log(@config, req, res)
55
+ end
56
+ end
57
+
58
+ break if @http_version < '1.1'
59
+ break unless req.keep_alive?
60
+ break unless res.keep_alive?
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,21 @@
1
+ require_relative 'object_serializer'
2
+ require_relative 'service_serializer'
3
+
4
+ module RubyHome
5
+ module HTTP
6
+ class AccessorySerializer
7
+ include ObjectSerializer
8
+
9
+ def root
10
+ 'accessories'
11
+ end
12
+
13
+ def record_hash(accessory)
14
+ {
15
+ 'aid' => accessory.id,
16
+ 'services' => ServiceSerializer.new(accessory.services).serializable_hash,
17
+ }
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ require_relative 'object_serializer'
2
+
3
+ module RubyHome
4
+ module HTTP
5
+ class CharacteristicSerializer
6
+ include ObjectSerializer
7
+
8
+ def record_hash(characteristic)
9
+ record_hash = {}
10
+
11
+ record_hash['iid'] = characteristic.instance_id
12
+ record_hash['type'] = characteristic.uuid
13
+ record_hash['perms'] = characteristic.properties.map do |property|
14
+ RubyHome::Characteristic::PROPERTIES[property]
15
+ end.compact
16
+ record_hash['format'] = characteristic.format
17
+ if characteristic.value != nil
18
+ record_hash['value'] = characteristic.value
19
+ end
20
+ record_hash['description'] = characteristic.description
21
+
22
+ record_hash
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,21 @@
1
+ require_relative 'object_serializer'
2
+
3
+ module RubyHome
4
+ module HTTP
5
+ class CharacteristicValueSerializer
6
+ include ObjectSerializer
7
+
8
+ def root
9
+ 'characteristics'
10
+ end
11
+
12
+ def record_hash(characteristic)
13
+ {
14
+ 'aid' => characteristic.accessory_id,
15
+ 'iid' => characteristic.instance_id,
16
+ 'value' => characteristic.value,
17
+ }
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,39 @@
1
+ require 'oj'
2
+
3
+ module RubyHome
4
+ module HTTP
5
+ module ObjectSerializer
6
+ def initialize(resource)
7
+ if resource.respond_to?(:each) && !resource.respond_to?(:each_pair)
8
+ @resources = resource
9
+ else
10
+ @resource = resource
11
+ end
12
+ end
13
+
14
+ def root
15
+ nil
16
+ end
17
+
18
+ def serializable_hash
19
+ serializable_hash = if @resource
20
+ record_hash(resource)
21
+ elsif @resources
22
+ @resources.map do |resource|
23
+ record_hash(resource)
24
+ end
25
+ end
26
+
27
+ if root
28
+ {root => serializable_hash}
29
+ else
30
+ serializable_hash
31
+ end
32
+ end
33
+
34
+ def serialized_json
35
+ Oj.dump(serializable_hash)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,20 @@
1
+ require_relative 'characteristic_serializer'
2
+ require_relative 'object_serializer'
3
+
4
+ module RubyHome
5
+ module HTTP
6
+ class ServiceSerializer
7
+ include ObjectSerializer
8
+
9
+ def record_hash(service)
10
+ {
11
+ 'iid' => service.instance_id,
12
+ 'type' => service.uuid,
13
+ 'characteristics' => CharacteristicSerializer.new(service.characteristics).serializable_hash,
14
+ 'primary' => service.primary,
15
+ 'hidden' => service.hidden,
16
+ }
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,59 @@
1
+ module RubyHome
2
+ class IdentifierCache
3
+ class << self
4
+ attr_accessor :accessories
5
+
6
+ def accessories
7
+ @@accessories ||= []
8
+ end
9
+
10
+ def reset!
11
+ @@accessories = []
12
+ end
13
+
14
+ def services
15
+ accessories.flat_map(&:services)
16
+ end
17
+
18
+ def characteristics
19
+ services.flat_map(&:characteristics)
20
+ end
21
+
22
+ def find_characteristics(attributes)
23
+ characteristics.select do |characteristic|
24
+ attributes.all? do |key, value|
25
+ characteristic.send(key) == value
26
+ end
27
+ end
28
+ end
29
+
30
+ def add_accessory(accessory)
31
+ return true if accessories.include?(accessory)
32
+
33
+ accessories << accessory.tap do |a|
34
+ a.id = accessories.size + 1
35
+ end
36
+
37
+ end
38
+
39
+ def add_service(service)
40
+ return true if services.include?(service)
41
+
42
+ accessory = service.accessory
43
+ accessory.services << service.tap do |s|
44
+ s.instance_id = accessory.next_available_instance_id
45
+ end
46
+
47
+ end
48
+
49
+ def add_characteristic(characteristic)
50
+ return true if characteristics.include?(characteristic)
51
+
52
+ service = characteristic.service
53
+ service.characteristics << characteristic.tap do |c|
54
+ c.instance_id = characteristic.accessory.next_available_instance_id
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end