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