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,58 @@
1
+ require 'rbnacl/libsodium'
2
+
3
+ module RubyHome
4
+ module HAP
5
+ class HTTPDecryption
6
+ AAD_LENGTH_BYTES = 2
7
+ AUTHENTICATE_TAG_LENGTH_BYTES = 16
8
+
9
+ def initialize(key, count: 0)
10
+ @key = key
11
+ @count = count
12
+ end
13
+
14
+ def decrypt(data)
15
+ decrypted_data = []
16
+ read_pointer = 0
17
+
18
+ while read_pointer < data.length
19
+ little_endian_length_of_encrypted_data = data[read_pointer...read_pointer+AAD_LENGTH_BYTES]
20
+ length_of_encrypted_data = little_endian_length_of_encrypted_data.unpack('v').first
21
+ read_pointer += AAD_LENGTH_BYTES
22
+
23
+ message = data[read_pointer...read_pointer+length_of_encrypted_data]
24
+ read_pointer += length_of_encrypted_data
25
+
26
+ auth_tag = data[read_pointer...read_pointer+AUTHENTICATE_TAG_LENGTH_BYTES]
27
+ read_pointer += AUTHENTICATE_TAG_LENGTH_BYTES
28
+
29
+ ciphertext = message + auth_tag
30
+ additional_data = little_endian_length_of_encrypted_data
31
+ decrypted_data << chacha20poly1305ietf.decrypt(nonce, ciphertext, additional_data)
32
+
33
+ increment_count!
34
+ end
35
+
36
+ decrypted_data
37
+ end
38
+
39
+ attr_reader :count
40
+
41
+ private
42
+
43
+ attr_reader :key
44
+
45
+ def increment_count!
46
+ @count += 1
47
+ end
48
+
49
+ def nonce
50
+ HexPad.pad([count].pack('Q<'))
51
+ end
52
+
53
+ def chacha20poly1305ietf
54
+ RbNaCl::AEAD::ChaCha20Poly1305IETF.new(key)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,43 @@
1
+ require 'rbnacl/libsodium'
2
+
3
+ module RubyHome
4
+ module HAP
5
+ class HTTPEncryption
6
+ def initialize(key, count: 0)
7
+ @key = key
8
+ @count = count
9
+ end
10
+
11
+ def encrypt(data)
12
+ data.chars.each_slice(1024).map(&:join).map do |message|
13
+ additional_data = [message.length].pack('v')
14
+
15
+ encrypted_data = chacha20poly1305ietf.encrypt(nonce, message, additional_data)
16
+ increment_count!
17
+
18
+ [additional_data, encrypted_data].join
19
+ end
20
+ end
21
+
22
+ attr_reader :count
23
+
24
+ private
25
+
26
+ attr_reader :key
27
+
28
+ def increment_count!
29
+ @count += 1
30
+ end
31
+
32
+ def nonce
33
+ HexPad.pad([count].pack('Q<'))
34
+ end
35
+
36
+ def chacha20poly1305ietf
37
+ RbNaCl::AEAD::ChaCha20Poly1305IETF.new(key)
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+
@@ -0,0 +1,38 @@
1
+ Dir[File.dirname(__FILE__) + '/services/*.rb'].each { |file| require file }
2
+ require_relative 'characteristic'
3
+
4
+ module RubyHome
5
+ class Service
6
+ def initialize(accessory: , primary: false, hidden: false, name:, description:, uuid:)
7
+ @accessory = accessory
8
+ @primary = primary
9
+ @hidden = hidden
10
+ @name = name
11
+ @description = description
12
+ @uuid = uuid
13
+ @characteristics = []
14
+ end
15
+
16
+ attr_reader :accessory, :characteristics, :primary, :hidden, :name, :description, :uuid
17
+ attr_accessor :instance_id
18
+
19
+ def characteristic(characteristic_name)
20
+ characteristics.find do |characteristic|
21
+ characteristic.name == characteristic_name
22
+ end
23
+ end
24
+
25
+ def save
26
+ IdentifierCache.add_accessory(accessory)
27
+ IdentifierCache.add_service(self)
28
+ end
29
+
30
+ def inspect
31
+ {
32
+ primary: primary,
33
+ hidden: hidden,
34
+ characteristics: characteristics
35
+ }
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,28 @@
1
+ require 'sinatra/base'
2
+ require_relative '../rack/handler/hap_server'
3
+ require_relative 'cache'
4
+
5
+ Rack::Handler.register 'hap_server', RubyHome::Rack::Handler::HAPServer
6
+
7
+ module RubyHome
8
+ module HTTP
9
+ class Application < Sinatra::Base
10
+ Dir[File.dirname(__FILE__) + '/controllers/*.rb'].each {|file| require file }
11
+
12
+ disable :protection
13
+ disable :logging
14
+ set :bind, '0.0.0.0'
15
+ set :quiet, true
16
+ set :server, :hap_server
17
+ set :server_settings, AcceptCallback: -> (sock) do
18
+ self.set :request_id, sock.object_id
19
+ end
20
+
21
+ use AccessoriesController
22
+ use CharacteristicsController
23
+ use PairSetupsController
24
+ use PairVerifiesController
25
+ use PairingsController
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+ require 'singleton'
2
+
3
+ module RubyHome
4
+ module ActLikeHash
5
+ def [](key)
6
+ store[key]
7
+ end
8
+
9
+ def []=(key, value)
10
+ store[key] = value
11
+ end
12
+ end
13
+
14
+ class Cache
15
+ include ActLikeHash
16
+
17
+ def store
18
+ @store ||= {}
19
+ end
20
+ end
21
+
22
+ class GlobalCache
23
+ include Singleton
24
+ include ActLikeHash
25
+
26
+ def store
27
+ @@store ||= {}
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,19 @@
1
+ require_relative 'application_controller'
2
+ require_relative '../serializers/accessory_serializer'
3
+
4
+ module RubyHome
5
+ module HTTP
6
+ class AccessoriesController < ApplicationController
7
+ get '/accessories' do
8
+ content_type 'application/hap+json'
9
+
10
+ if cache[:controller_to_accessory_key] && cache[:accessory_to_controller_key]
11
+ AccessorySerializer.new(identifier_cache.accessories).serialized_json
12
+ else
13
+ status 401
14
+ JSON.generate({"status" => -70401})
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,37 @@
1
+ module RubyHome
2
+ module HTTP
3
+ class ApplicationController < Sinatra::Base
4
+ disable :protection
5
+
6
+ def unpack_request
7
+ @_unpack_request ||= begin
8
+ request.body.rewind
9
+ TLV.unpack(request.body.read)
10
+ end
11
+ end
12
+
13
+ def json_body
14
+ @_json_body ||= begin
15
+ request.body.rewind
16
+ JSON.parse(request.body.read)
17
+ end
18
+ end
19
+
20
+ def accessory_info
21
+ AccessoryInfo
22
+ end
23
+
24
+ def identifier_cache
25
+ IdentifierCache
26
+ end
27
+
28
+ def request_id
29
+ Application.request_id
30
+ end
31
+
32
+ def cache
33
+ GlobalCache.instance[request_id] ||= Cache.new
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,49 @@
1
+ require_relative 'application_controller'
2
+ require_relative '../serializers/characteristic_value_serializer'
3
+
4
+ module RubyHome
5
+ module HTTP
6
+ class CharacteristicsController < ApplicationController
7
+ get '/characteristics' do
8
+ content_type 'application/hap+json'
9
+
10
+ if cache[:controller_to_accessory_key] && cache[:accessory_to_controller_key]
11
+ accessory_id, instance_id = params[:id].split('.')
12
+ characteristics = IdentifierCache.find_characteristics(
13
+ accessory_id: accessory_id.to_i,
14
+ instance_id: instance_id.to_i
15
+ )
16
+
17
+ CharacteristicValueSerializer.new(characteristics).serialized_json
18
+ else
19
+ status 401
20
+ JSON.generate({"status" => -70401})
21
+ end
22
+ end
23
+
24
+ put '/characteristics' do
25
+ content_type 'application/hap+json'
26
+
27
+ if cache[:controller_to_accessory_key] && cache[:accessory_to_controller_key]
28
+ json_body.fetch('characteristics', []).each do |characteristic_params|
29
+ accessory_id = characteristic_params['aid']
30
+ instance_id = characteristic_params['iid']
31
+ characteristic = IdentifierCache.find_characteristics(
32
+ accessory_id: accessory_id.to_i,
33
+ instance_id: instance_id.to_i
34
+ ).first
35
+
36
+ if characteristic_params['value']
37
+ characteristic.value = characteristic_params['value']
38
+ end
39
+ end
40
+
41
+ status 204
42
+ else
43
+ status 401
44
+ JSON.generate({"status" => -70401})
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,146 @@
1
+ require 'hkdf'
2
+ require 'openssl'
3
+ require 'rbnacl/libsodium'
4
+ require 'ruby_home-srp'
5
+ require_relative '../../hap/hex_pad'
6
+ require_relative '../../tlv'
7
+ require_relative 'application_controller'
8
+
9
+ module RubyHome
10
+ module HTTP
11
+ class PairSetupsController < ApplicationController
12
+ post '/pair-setup' do
13
+ content_type 'application/pairing+tlv8'
14
+
15
+ case unpack_request['kTLVType_State']
16
+ when 1
17
+ srp_start_response
18
+ when 3
19
+ srp_verify_response
20
+ when 5
21
+ exchange_response
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def srp_start_response
28
+ username = 'Pair-Setup'
29
+ password = '031-45-154'
30
+
31
+ auth = srp_verifier.generate_userauth(username, password)
32
+
33
+ verifier = auth[:verifier]
34
+ salt = auth[:salt]
35
+
36
+ challenge_and_proof = srp_verifier.get_challenge_and_proof(username, verifier, salt)
37
+ store_proof(challenge_and_proof[:proof])
38
+
39
+ TLV.pack({
40
+ 'kTLVType_Salt' => challenge_and_proof[:challenge][:salt],
41
+ 'kTLVType_PublicKey' => challenge_and_proof[:challenge][:B],
42
+ 'kTLVType_State' => 2
43
+ })
44
+ end
45
+
46
+ def srp_verify_response
47
+ proof = retrieve_proof.dup
48
+ proof[:A] = unpack_request['kTLVType_PublicKey']
49
+
50
+ client_m1_proof = unpack_request['kTLVType_Proof']
51
+ server_m2_proof = srp_verifier.verify_session(proof, unpack_request['kTLVType_Proof'])
52
+
53
+ store_session_key(srp_verifier.K)
54
+ forget_proof!
55
+
56
+ TLV.pack({
57
+ 'kTLVType_State' => 4,
58
+ 'kTLVType_Proof' => server_m2_proof
59
+ })
60
+ end
61
+
62
+ def exchange_response
63
+ encrypted_data = unpack_request['kTLVType_EncryptedData']
64
+
65
+ hkdf = HAP::HKDFEncryption.new(info: 'Pair-Setup-Encrypt-Info', salt: 'Pair-Setup-Encrypt-Salt')
66
+ key = hkdf.encrypt([session_key].pack('H*'))
67
+
68
+ chacha20poly1305ietf = RbNaCl::AEAD::ChaCha20Poly1305IETF.new(key)
69
+
70
+ nonce = HAP::HexPad.pad('PS-Msg05')
71
+ decrypted_data = chacha20poly1305ietf.decrypt(nonce, [encrypted_data].pack('H*'), nil)
72
+ unpacked_decrypted_data = TLV.unpack(decrypted_data)
73
+
74
+ iosdevicepairingid = unpacked_decrypted_data['kTLVType_Identifier']
75
+ iosdevicesignature = unpacked_decrypted_data['kTLVType_Signature']
76
+ iosdeviceltpk = unpacked_decrypted_data['kTLVType_PublicKey']
77
+
78
+ hkdf = HAP::HKDFEncryption.new(info: 'Pair-Setup-Controller-Sign-Info', salt: 'Pair-Setup-Controller-Sign-Salt')
79
+ iosdevicex = hkdf.encrypt([session_key].pack('H*'))
80
+
81
+ iosdeviceinfo = [
82
+ iosdevicex.unpack('H*'),
83
+ TLV::Utf8.pack(iosdevicepairingid),
84
+ iosdeviceltpk
85
+ ].join
86
+ verify_key = RbNaCl::Signatures::Ed25519::VerifyKey.new([iosdeviceltpk].pack('H*'))
87
+
88
+ if verify_key.verify([iosdevicesignature].pack('H*'), [iosdeviceinfo].pack('H*'))
89
+ hkdf = HAP::HKDFEncryption.new(info: 'Pair-Setup-Accessory-Sign-Info', salt: 'Pair-Setup-Accessory-Sign-Salt')
90
+ accessory_x = hkdf.encrypt([session_key].pack('H*'))
91
+
92
+ signing_key = accessory_info.signing_key
93
+ accessoryltpk = signing_key.verify_key.to_bytes.unpack('H*')[0]
94
+ accessoryinfo = [
95
+ accessory_x.unpack('H*'),
96
+ TLV::Utf8.pack(accessory_info.device_id),
97
+ accessoryltpk
98
+ ].join
99
+
100
+ accessorysignature = signing_key.sign([accessoryinfo].pack('H*')).unpack('H*')[0]
101
+
102
+ subtlv = TLV.pack({
103
+ 'kTLVType_Identifier' => accessory_info.device_id,
104
+ 'kTLVType_PublicKey' => accessoryltpk,
105
+ 'kTLVType_Signature' => accessorysignature
106
+ })
107
+
108
+ nonce = HAP::HexPad.pad('PS-Msg06')
109
+ encrypted_data = chacha20poly1305ietf.encrypt(nonce, subtlv, nil).unpack('H*')[0]
110
+
111
+ pairing_params = { admin: true, identifier: iosdevicepairingid, public_key: iosdeviceltpk }
112
+ accessory_info.add_paired_client pairing_params
113
+
114
+ TLV.pack({
115
+ 'kTLVType_State' => 6,
116
+ 'kTLVType_EncryptedData' => encrypted_data
117
+ })
118
+ end
119
+ end
120
+
121
+ def srp_verifier
122
+ @_verifier ||= RubyHome::SRP::Verifier.new
123
+ end
124
+
125
+ def store_proof(proof)
126
+ cache[:proof] = proof
127
+ end
128
+
129
+ def retrieve_proof
130
+ cache[:proof]
131
+ end
132
+
133
+ def forget_proof!
134
+ cache[:proof] = nil
135
+ end
136
+
137
+ def store_session_key(key)
138
+ cache[:session_key] = key
139
+ end
140
+
141
+ def session_key
142
+ cache[:session_key]
143
+ end
144
+ end
145
+ end
146
+ end