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.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.hound.yml +2 -0
- data/.rspec +3 -0
- data/.rubocop.yml +9 -0
- data/.travis.yml +21 -0
- data/Gemfile +6 -0
- data/LICENSE +21 -0
- data/README.md +36 -0
- data/Rakefile +7 -0
- data/bin/console +14 -0
- data/bin/rubyhome +16 -0
- data/bin/setup +8 -0
- data/lib/ruby_home.rb +6 -0
- data/lib/ruby_home/accessory_info.rb +99 -0
- data/lib/ruby_home/broadcast.rb +31 -0
- data/lib/ruby_home/config/characteristics.yml +1692 -0
- data/lib/ruby_home/config/services.yml +416 -0
- data/lib/ruby_home/device_id.rb +30 -0
- data/lib/ruby_home/dns/service.rb +44 -0
- data/lib/ruby_home/dns/text_record.rb +100 -0
- data/lib/ruby_home/factories/accessory_factory.rb +78 -0
- data/lib/ruby_home/factories/characteristic_factory.rb +57 -0
- data/lib/ruby_home/factories/templates/characteristic_template.rb +43 -0
- data/lib/ruby_home/factories/templates/service_template.rb +50 -0
- data/lib/ruby_home/hap/accessory.rb +26 -0
- data/lib/ruby_home/hap/characteristic.rb +60 -0
- data/lib/ruby_home/hap/hex_pad.rb +13 -0
- data/lib/ruby_home/hap/hkdf_encryption.rb +34 -0
- data/lib/ruby_home/hap/http_decryption.rb +58 -0
- data/lib/ruby_home/hap/http_encryption.rb +43 -0
- data/lib/ruby_home/hap/service.rb +38 -0
- data/lib/ruby_home/http/application.rb +28 -0
- data/lib/ruby_home/http/cache.rb +30 -0
- data/lib/ruby_home/http/controllers/accessories_controller.rb +19 -0
- data/lib/ruby_home/http/controllers/application_controller.rb +37 -0
- data/lib/ruby_home/http/controllers/characteristics_controller.rb +49 -0
- data/lib/ruby_home/http/controllers/pair_setups_controller.rb +146 -0
- data/lib/ruby_home/http/controllers/pair_verifies_controller.rb +81 -0
- data/lib/ruby_home/http/controllers/pairings_controller.rb +38 -0
- data/lib/ruby_home/http/hap_request.rb +56 -0
- data/lib/ruby_home/http/hap_response.rb +57 -0
- data/lib/ruby_home/http/hap_server.rb +65 -0
- data/lib/ruby_home/http/serializers/accessory_serializer.rb +21 -0
- data/lib/ruby_home/http/serializers/characteristic_serializer.rb +26 -0
- data/lib/ruby_home/http/serializers/characteristic_value_serializer.rb +21 -0
- data/lib/ruby_home/http/serializers/object_serializer.rb +39 -0
- data/lib/ruby_home/http/serializers/service_serializer.rb +20 -0
- data/lib/ruby_home/identifier_cache.rb +59 -0
- data/lib/ruby_home/rack/handler/hap_server.rb +26 -0
- data/lib/ruby_home/tlv.rb +83 -0
- data/lib/ruby_home/tlv/bytes.rb +19 -0
- data/lib/ruby_home/tlv/int.rb +15 -0
- data/lib/ruby_home/tlv/utf8.rb +18 -0
- data/lib/ruby_home/version.rb +3 -0
- data/rubyhome.gemspec +43 -0
- data/sbin/characteristic_generator.rb +83 -0
- data/sbin/service_generator.rb +69 -0
- 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
|