ruby_home 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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