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,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