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