hap_client 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1563b1509e5f1e6f88c06ecdf588fdd43c75dddb82e3b3dbeb1090d4bcc9146b
4
+ data.tar.gz: e6968e093e0b00a77e4c046783dfecaa86aeefe2f10a5ecd224000e99d48cb33
5
+ SHA512:
6
+ metadata.gz: 017ca3ba5512e19e51ddc1031f5f478d1cf40bf564593a06ddc0dded6cc25156ab6c9c2e0d53a38a11956b47118c427c285669767467ee7617faaa6af613c7b5
7
+ data.tar.gz: 7ede4a9834f3d7e1fe4f66b2bd3ec9c83b4e9ff332a7fd82e415f3ee89a41e72e77fdb04efda19f73d52edb0b8415cac7c7b0129a85909d9eb93e08fdf3a06f1
data/.gitignore ADDED
@@ -0,0 +1,55 @@
1
+ *.gem
2
+ *.rbc
3
+ Gemfile.lock
4
+ /.config
5
+ /coverage/
6
+ /InstalledFiles
7
+ /pkg/
8
+ /spec/reports/
9
+ /spec/examples.txt
10
+ /test/tmp/
11
+ /test/version_tmp/
12
+ /tmp/
13
+
14
+ # Used by dotenv library to load environment variables.
15
+ # .env
16
+
17
+ ## Specific to RubyMotion:
18
+ .dat*
19
+ .repl_history
20
+ build/
21
+ *.bridgesupport
22
+ build-iPhoneOS/
23
+ build-iPhoneSimulator/
24
+
25
+ ## Specific to RubyMotion (use of CocoaPods):
26
+ #
27
+ # We recommend against adding the Pods directory to your .gitignore. However
28
+ # you should judge for yourself, the pros and cons are mentioned at:
29
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
30
+ #
31
+ # vendor/Pods/
32
+
33
+ ## Documentation cache and generated files:
34
+ /.yardoc/
35
+ /_yardoc/
36
+ /doc/
37
+ /rdoc/
38
+
39
+ ## Environment normalization:
40
+ /.bundle/
41
+ /vendor/bundle
42
+ /lib/bundler/man/
43
+
44
+ # for a library or gem, you might want to ignore these files since the code is
45
+ # intended to run in multiple environments; otherwise, check them in:
46
+ # Gemfile.lock
47
+ # .ruby-version
48
+ .ruby-gemset
49
+
50
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
51
+ .rvmrc
52
+
53
+ # Emacs
54
+ *~
55
+ .#*
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.5.1
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Seluxit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # hap_client
2
+ Ruby Gem for Apple Homekit Client
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
2
+ require "hap_client/version"
3
+
4
+ task :build do
5
+ system "gem build hap_client.gemspec"
6
+ end
7
+
8
+ task :release => :build do
9
+ system "gem push hap_client-#{HapClient::VERSION}.gem"
10
+ system "rm hap_client-#{HapClient::VERSION}.gem"
11
+ end
@@ -0,0 +1,29 @@
1
+ lib = File.expand_path('../lib/', __FILE__)
2
+ $:.unshift lib unless $:.include?(lib)
3
+
4
+ require 'hap_client/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = 'hap_client'
8
+ s.version = HapClient::VERSION
9
+ s.date = '2018-06-15'
10
+ s.summary = "HAP client"
11
+ s.description = "Ruby Gem for Apple Homekit Client"
12
+ s.authors = ["Andreas Bomholtz"]
13
+ s.email = 'andreas@seluxit.com'
14
+ s.files = `git ls-files -z`.split("\x0").reject do |f|
15
+ f.match(%r{^(test|spec|features)/})
16
+ end
17
+ s.homepage = 'http://github.com/Seluxit/hap_client'
18
+ s.license = 'MIT'
19
+
20
+ s.add_dependency "eventmachine", '~> 1.2'
21
+ s.add_dependency "http_parser.rb", '~> 0.6'
22
+ s.add_dependency "json", '~> 2.1'
23
+ s.add_dependency 'ruby_home', '0.1.2'
24
+ s.add_dependency "ruby_home-srp", '1.2.0'
25
+
26
+ s.add_development_dependency 'bundler', '~> 1.16'
27
+ s.add_development_dependency 'rake', '~> 12.3'
28
+ s.add_development_dependency 'rspec', '~> 3.0'
29
+ end
@@ -0,0 +1,71 @@
1
+ module HAP
2
+ module EncryptionRequest
3
+ AAD_LENGTH_BYTES = 2
4
+ AUTHENTICATE_TAG_LENGTH_BYTES = 16
5
+
6
+ attr_reader :encryption_count, :decryption_count
7
+
8
+ def encryption_ready?()
9
+ return !@controller_to_accessory_key.nil?
10
+ end
11
+
12
+ private
13
+
14
+ def encrypt(data)
15
+ @encryption_count ||= 0
16
+
17
+ data.chars.each_slice(1024).map(&:join).map do |message|
18
+ additional_data = [message.length].pack('v')
19
+
20
+ chacha20poly1305ietf = RubyHome::HAP::Crypto::ChaCha20Poly1305.new(@controller_to_accessory_key)
21
+ encrypted_data = chacha20poly1305ietf.encrypt(encryption_nonce, message, additional_data)
22
+ increment_encryption_count!
23
+
24
+ [additional_data, encrypted_data].join
25
+ end
26
+ end
27
+
28
+ def decrypt(data)
29
+ @decryption_count ||= 0
30
+ decrypted_data = []
31
+ read_pointer = 0
32
+
33
+ while read_pointer < data.length
34
+ little_endian_length_of_encrypted_data = data[read_pointer...read_pointer+AAD_LENGTH_BYTES]
35
+ length_of_encrypted_data = little_endian_length_of_encrypted_data.unpack('v').first
36
+ read_pointer += AAD_LENGTH_BYTES
37
+
38
+ message = data[read_pointer...read_pointer+length_of_encrypted_data]
39
+ read_pointer += length_of_encrypted_data
40
+
41
+ auth_tag = data[read_pointer...read_pointer+AUTHENTICATE_TAG_LENGTH_BYTES]
42
+ read_pointer += AUTHENTICATE_TAG_LENGTH_BYTES
43
+
44
+ ciphertext = message + auth_tag
45
+ additional_data = little_endian_length_of_encrypted_data
46
+ chacha20poly1305ietf = RubyHome::HAP::Crypto::ChaCha20Poly1305.new(@accessory_to_controller_key)
47
+ decrypted_data << chacha20poly1305ietf.decrypt(decryption_nonce, ciphertext, additional_data)
48
+
49
+ increment_decryption_count!
50
+ end
51
+
52
+ decrypted_data.join
53
+ end
54
+
55
+ def increment_encryption_count!
56
+ @encryption_count += 1
57
+ end
58
+
59
+ def encryption_nonce
60
+ RubyHome::HAP::HexPad.pad([encryption_count].pack('Q<'))
61
+ end
62
+
63
+ def increment_decryption_count!
64
+ @decryption_count += 1
65
+ end
66
+
67
+ def decryption_nonce
68
+ RubyHome::HAP::HexPad.pad([decryption_count].pack('Q<'))
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,40 @@
1
+ require 'logger'
2
+
3
+ module HAP
4
+ module Log
5
+ LOG_LVL = ENV['DEBUG'] ? :debug : :info
6
+
7
+ def init_log()
8
+ @log = Logger.new(STDOUT,
9
+ level: LOG_LVL,
10
+ progname: self,
11
+ formatter: proc {|severity, datetime, progname, msg|
12
+ "[#{datetime}][#{progname}] #{severity}: #{msg}\n"
13
+ })
14
+ end
15
+
16
+ def fatal(msg)
17
+ @log.fatal(msg)
18
+ end
19
+
20
+ def error(msg)
21
+ @log.error(msg)
22
+ end
23
+
24
+ def warn(msg)
25
+ @log.warn(msg)
26
+ end
27
+
28
+ def info(msg)
29
+ @log.info(msg)
30
+ end
31
+
32
+ def debug(msg)
33
+ @log.debug(msg)
34
+ end
35
+
36
+ def log_debug?
37
+ @log.debug?
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,379 @@
1
+ require 'ruby_home/device_id'
2
+ require 'ruby_home/hap/tlv'
3
+ require 'ruby_home/hap/hex_pad'
4
+ require 'ruby_home/hap/crypto/chacha20poly1305'
5
+ require 'hkdf'
6
+ require 'ruby_home/hap/crypto/hkdf'
7
+ require 'ruby_home-srp'
8
+ require 'ed25519'
9
+ require 'x25519'
10
+
11
+ class PairingError < StandardError
12
+ end
13
+
14
+ module HAP
15
+ module Pairing
16
+ ERROR_NAMES = {
17
+ 1 => 'kTLVError_Unknown',
18
+ 2 => 'kTLVError_Authentication',
19
+ 3 => 'kTLVError_Backoff',
20
+ 4 => 'kTLVError_MaxPeers',
21
+ 5 => 'kTLVError_MaxTries',
22
+ 6 => 'kTLVError_Unavailable',
23
+ 7 => 'kTLVError_Busy',
24
+ }.freeze
25
+ ERROR_TYPES = ERROR_NAMES.invert.freeze
26
+
27
+ def pair_setup(password, &block)
28
+ info("Pair Setup Step 1/3")
29
+ @mode = :pair_setup
30
+ @password = password
31
+ srp_start_request()
32
+
33
+ if block_given?
34
+ @pair_setup_callback = block
35
+ end
36
+ end
37
+
38
+ def pair_verify(&block)
39
+ info("Pair Verify 1/2")
40
+ @mode = :pair_verify
41
+ verify_start_request()
42
+
43
+ if block_given?
44
+ @pair_verify_callback = block
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def pair_setup_parse(data)
51
+ begin
52
+ response = check_tlv_response(data)
53
+
54
+ case response['kTLVType_State']
55
+ when 2
56
+ info("Pair Setup Step 2/3")
57
+ srp_verify_request(response, @password)
58
+ when 4
59
+ srp_verify(response)
60
+
61
+ info("Pair Setup Step 3/3")
62
+ srp_exchange_request()
63
+ when 6
64
+ info("Verifying Server Exchange")
65
+ srp_exchange_verify(response)
66
+
67
+ call_pair_setup_callback(true)
68
+ else
69
+ error("Unknown Pair Setup State: #{response['kTLVType_State']}")
70
+ end
71
+ rescue PairingError => e
72
+ error("Pair Setup Error: #{e}")
73
+ call_pair_setup_callback(false, e.to_s)
74
+ end
75
+ end
76
+
77
+ def call_pair_setup_callback(status, data=nil)
78
+ if @pair_setup_callback
79
+ t = @pair_setup_callback
80
+ @pair_setup_callback = nil
81
+ t.call(status, data)
82
+ end
83
+ end
84
+
85
+ def srp_start_request()
86
+ debug("Pair Setup SRP Start Request")
87
+ data = RubyHome::HAP::TLV.encode({
88
+ 'kTLVType_State' => 0x01,
89
+ 'kTLVType_Method' => 0x00
90
+ })
91
+ post("/pair-setup", "application/pairing+tlv8", data)
92
+ end
93
+
94
+ def srp_verify_request(response, password)
95
+ debug("Pair Setup SRP Verify Request")
96
+
97
+ username = 'Pair-Setup'
98
+ debug("Using #{password} to pair with device")
99
+
100
+ # convert bin variables to hex strings
101
+ salt = bin_to_hex(response["kTLVType_Salt"])
102
+ serverPublicKey = bin_to_hex(response["kTLVType_PublicKey"])
103
+
104
+ debug("Generating Client Public/Private Keys")
105
+ @srp_client = RubyHome::SRP::Client.new(3072)
106
+ clientPublicKey = hex_to_bin(@srp_client.start_authentication())
107
+
108
+ debug("Process Challenge from Server")
109
+ client_M = hex_to_bin(@srp_client.process_challenge(username, password, salt, serverPublicKey))
110
+
111
+ debug("Send Client Proof to Server")
112
+ data = RubyHome::HAP::TLV.encode({
113
+ 'kTLVType_Proof' => client_M,
114
+ 'kTLVType_PublicKey' => clientPublicKey,
115
+ 'kTLVType_State' => 3,
116
+ 'kTLVType_Method' => 0
117
+ })
118
+
119
+ # Save session key
120
+ @srp_session_key = @srp_client.K
121
+
122
+ post("/pair-setup", "application/pairing+tlv8", data)
123
+ end
124
+
125
+ def srp_verify(response)
126
+ debug("Verifying Server Proof")
127
+ serverProof = bin_to_hex(response['kTLVType_Proof'])
128
+
129
+ unless @srp_client.verify(serverProof)
130
+ raise PairingError, "Failed to verify server proof"
131
+ end
132
+
133
+ @srp_client = nil
134
+ end
135
+
136
+ def srp_exchange_request()
137
+ debug("Pair Setup SRP Exchange Request")
138
+
139
+ debug("Generate Longterm key")
140
+ @signature_key = Ed25519::SigningKey.generate.to_bytes.unpack1('H*')
141
+ @signing_key = Ed25519::SigningKey.new([@signature_key].pack('H*'))
142
+
143
+ debug("Generating device id")
144
+ @client_id = RubyHome::DeviceID.generate()
145
+
146
+ debug("Generating Encryption key")
147
+ hkdf = RubyHome::HAP::Crypto::HKDF.new(info: 'Pair-Setup-Encrypt-Info', salt: 'Pair-Setup-Encrypt-Salt')
148
+ key = hkdf.encrypt(@srp_session_key)
149
+ @chacha20poly1305ietf = RubyHome::HAP::Crypto::ChaCha20Poly1305.new(key)
150
+
151
+ debug("Generating ClientX")
152
+ hkdf = RubyHome::HAP::Crypto::HKDF.new(info: 'Pair-Setup-Controller-Sign-Info', salt: 'Pair-Setup-Controller-Sign-Salt')
153
+ clientX = hkdf.encrypt(@srp_session_key)
154
+
155
+ debug("Generating ClientInfo")
156
+ clientLTPK = @signing_key.verify_key.to_bytes
157
+ clientInfo = [
158
+ clientX.unpack1('H*'),
159
+ @client_id.unpack1('H*'),
160
+ clientLTPK.unpack1('H*')
161
+ ].join
162
+
163
+ debug("Generating Client Signature")
164
+ clientSignature = @signing_key.sign([clientInfo].pack('H*'))
165
+
166
+ debug("Generating Encrypted Data")
167
+ subtlv = RubyHome::HAP::TLV.encode({
168
+ 'kTLVType_Identifier' => @client_id,
169
+ 'kTLVType_PublicKey' => clientLTPK,
170
+ 'kTLVType_Signature' => clientSignature
171
+ })
172
+ nonce = RubyHome::HAP::HexPad.pad('PS-Msg05')
173
+ encrypted_data = @chacha20poly1305ietf.encrypt(nonce, subtlv)
174
+
175
+ debug("Sending Encrypted Request to Server")
176
+ data = RubyHome::HAP::TLV.encode({
177
+ 'kTLVType_State' => 5,
178
+ 'kTLVType_EncryptedData' => encrypted_data
179
+ })
180
+ post("/pair-setup", "application/pairing+tlv8", data)
181
+ end
182
+
183
+ def srp_exchange_verify(response)
184
+ debug("Decrypting Server Response")
185
+ encrypted_data = response['kTLVType_EncryptedData']
186
+ nonce = RubyHome::HAP::HexPad.pad('PS-Msg06')
187
+
188
+ decrypted_data = @chacha20poly1305ietf.decrypt(nonce, encrypted_data)
189
+ unpacked_decrypted_data = RubyHome::HAP::TLV.read(decrypted_data)
190
+ @chacha20poly1305ietf = nil
191
+
192
+ debug("Verifying Server Signature")
193
+ @serverPairingId = unpacked_decrypted_data['kTLVType_Identifier']
194
+ serverSignature = unpacked_decrypted_data['kTLVType_Signature']
195
+ @accessoryltpk = unpacked_decrypted_data['kTLVType_PublicKey']
196
+
197
+ hkdf = RubyHome::HAP::Crypto::HKDF.new(info: 'Pair-Setup-Accessory-Sign-Info', salt: 'Pair-Setup-Accessory-Sign-Salt')
198
+ accessoryx = hkdf.encrypt(@srp_session_key)
199
+
200
+ accessoryinfo = [
201
+ accessoryx.unpack1('H*'),
202
+ @serverPairingId.unpack1('H*'),
203
+ @accessoryltpk.unpack1('H*')
204
+ ].join
205
+ verify_key = RbNaCl::Signatures::Ed25519::VerifyKey.new(@accessoryltpk)
206
+
207
+ begin
208
+ if verify_key.verify(serverSignature, [accessoryinfo].pack('H*'))
209
+ info("Pairing Success! Server Pairing ID: #{@serverPairingId}")
210
+ else
211
+ error("Failed to verify Server Signature")
212
+ raise PairingError, "Failed to verify Server Signature"
213
+ end
214
+ rescue RbNaCl::BadSignatureError
215
+ error("Failed to verify Server Signature")
216
+ raise PairingError, "Failed to verify Server Signature"
217
+ end
218
+ end
219
+
220
+ def pair_verify_parse(data)
221
+ begin
222
+ response = check_tlv_response(data)
223
+
224
+ case response['kTLVType_State']
225
+ when 2
226
+ info("Pair Verify 2/2")
227
+ verify_finish_request(response)
228
+ when 4
229
+ verify_finish_verify()
230
+ @mode = :paired
231
+
232
+ call_pair_verify_callback(true)
233
+ else
234
+ error("Unknown Pair Verify State: #{response['kTLVType_State']}")
235
+ end
236
+ rescue PairingError => e
237
+ error("Pair Verify Error: #{e}")
238
+ call_pair_verify_callback(false, e.to_s)
239
+ end
240
+ end
241
+
242
+ def call_pair_verify_callback(status, data=nil)
243
+ if @pair_verify_callback
244
+ t = @pair_verify_callback
245
+ @pair_verify_callback = nil
246
+ t.call(status, data)
247
+ end
248
+ end
249
+
250
+ def verify_start_request()
251
+ debug("Generating new Session Public/Private Keys")
252
+ @client_secret_key = X25519::Scalar.generate
253
+ @client_public_key = @client_secret_key.public_key.to_bytes
254
+
255
+ debug("Sending verify Request to Server")
256
+ data = RubyHome::HAP::TLV.encode({
257
+ 'kTLVType_State' => 1,
258
+ 'kTLVType_PublicKey' => @client_public_key
259
+ })
260
+ post("/pair-verify", "application/pairing+tlv8", data)
261
+ end
262
+
263
+ def verify_finish_request(response)
264
+ debug("Generating shared secret")
265
+ server_public_key = X25519::MontgomeryU.new(response['kTLVType_PublicKey'])
266
+ @shared_secret = @client_secret_key.multiply(server_public_key).to_bytes
267
+
268
+ debug("Generating session key")
269
+ hkdf = RubyHome::HAP::Crypto::HKDF.new(info: 'Pair-Verify-Encrypt-Info', salt: 'Pair-Verify-Encrypt-Salt')
270
+ session_key = hkdf.encrypt(@shared_secret)
271
+
272
+ debug("Decrypting data")
273
+ subtlv = response['kTLVType_EncryptedData']
274
+ chacha20poly1305ietf = RubyHome::HAP::Crypto::ChaCha20Poly1305.new(session_key)
275
+ nonce = RubyHome::HAP::HexPad.pad('PV-Msg02')
276
+ decrypted_data = chacha20poly1305ietf.decrypt(nonce, subtlv)
277
+ decrypted_data = RubyHome::HAP::TLV.read(decrypted_data)
278
+
279
+ debug("Verifying Server Signature")
280
+ server_device_id = decrypted_data['kTLVType_Identifier']
281
+ serverSignature = decrypted_data['kTLVType_Signature']
282
+
283
+ accessoryinfo = [
284
+ server_public_key.to_bytes.unpack1('H*'),
285
+ server_device_id.unpack1('H*'),
286
+ @client_public_key.unpack1('H*')
287
+ ].join
288
+ verify_key = RbNaCl::Signatures::Ed25519::VerifyKey.new(@accessoryltpk)
289
+
290
+ begin
291
+ if !verify_key.verify(serverSignature, [accessoryinfo].pack('H*'))
292
+ error("Server signature INVALID!")
293
+ raise PairingError, "Server signature INVALID!"
294
+ end
295
+ rescue RbNaCl::BadSignatureError
296
+ error("Server signature INVALID!")
297
+ raise PairingError, "Server signature INVALID!"
298
+ end
299
+
300
+ debug("Generating Client Info")
301
+ clientInfo = [
302
+ @client_public_key.unpack1('H*'),
303
+ @client_id.unpack1('H*'),
304
+ server_public_key.to_bytes.unpack1('H*')
305
+ ].join
306
+
307
+ debug("Generating Client Signature")
308
+ clientSignature = @signing_key.sign([clientInfo].pack('H*'))
309
+
310
+ debug("Generating Encrypted Data")
311
+ subtlv = RubyHome::HAP::TLV.encode({
312
+ 'kTLVType_Identifier' => @client_id,
313
+ 'kTLVType_Signature' => clientSignature
314
+ })
315
+
316
+ chacha20poly1305ietf = RubyHome::HAP::Crypto::ChaCha20Poly1305.new(session_key)
317
+ nonce = RubyHome::HAP::HexPad.pad('PV-Msg03')
318
+ encrypted_data = chacha20poly1305ietf.encrypt(nonce, subtlv)
319
+
320
+ debug("Sending Encrypted Request to Server")
321
+ data = RubyHome::HAP::TLV.encode({
322
+ 'kTLVType_State' => 3,
323
+ 'kTLVType_EncryptedData' => encrypted_data
324
+ })
325
+
326
+ post("/pair-verify", "application/pairing+tlv8", data)
327
+ end
328
+
329
+ def verify_finish_verify()
330
+ hkdf = RubyHome::HAP::Crypto::HKDF.new(info: 'Control-Write-Encryption-Key', salt: 'Control-Salt')
331
+ @controller_to_accessory_key = hkdf.encrypt(@shared_secret)
332
+
333
+ hkdf = RubyHome::HAP::Crypto::HKDF.new(info: 'Control-Read-Encryption-Key', salt: 'Control-Salt')
334
+ @accessory_to_controller_key = hkdf.encrypt(@shared_secret)
335
+
336
+ @shared_secret = nil
337
+
338
+ info("Pair Verify Complete")
339
+ end
340
+
341
+ def get_pairing_context()
342
+ {
343
+ 'client_id' => @client_id,
344
+ 'signature_key' => @signature_key,
345
+ 'accessoryltpk' => @accessoryltpk.unpack1('H*')
346
+ }
347
+ end
348
+
349
+ def set_pairing_context(context)
350
+ context = JSON.parse(context) if context.is_a?(String)
351
+ @client_id = context['client_id']
352
+ @signature_key = context['signature_key']
353
+ @accessoryltpk = hex_to_bin(context['accessoryltpk'])
354
+
355
+ @signing_key = Ed25519::SigningKey.new([@signature_key].pack('H*'))
356
+ end
357
+
358
+ def check_tlv_response(data)
359
+ data = RubyHome::HAP::TLV.read(data)
360
+
361
+ debug("Response: " + data.to_s)
362
+
363
+ if data['kTLVType_Error']
364
+ error("Failed to pair: #{data}")
365
+ raise PairingError, ERROR_NAMES[data['kTLVType_Error']]
366
+ end
367
+
368
+ return data
369
+ end
370
+
371
+ def bin_to_hex(s)
372
+ s.unpack('H*')[0]
373
+ end
374
+
375
+ def hex_to_bin(s)
376
+ s.scan(/../).map { |x| x.hex.chr }.join
377
+ end
378
+ end
379
+ end
@@ -0,0 +1,35 @@
1
+ require 'http/parser'
2
+
3
+ module HAP
4
+ module Parser
5
+ def init_parser
6
+ @parser = Http::Parser.new(self)
7
+ end
8
+
9
+ def receive_data(data)
10
+ if encryption_ready?
11
+ data = decrypt(data)
12
+ end
13
+
14
+ @parser << data
15
+ end
16
+
17
+ def on_message_begin
18
+ @headers = nil
19
+ @body = ''
20
+ end
21
+
22
+ def on_headers_complete(headers)
23
+ @headers = headers
24
+ end
25
+
26
+ def on_body(chunk)
27
+ @body << chunk
28
+ end
29
+
30
+ def on_message_complete
31
+ parse_message(@body)
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,68 @@
1
+ require_relative 'encryption_request'
2
+
3
+ module HAP
4
+ module Request
5
+ include EncryptionRequest
6
+
7
+ def get(url)
8
+ request("GET", url)
9
+ end
10
+
11
+ def post(url, type, data)
12
+ request("POST", url, type, data)
13
+ end
14
+
15
+ def put(url, type, data)
16
+ request("PUT", url, type, data)
17
+ end
18
+
19
+ private
20
+
21
+ def request(method, url, type=nil, data=nil)
22
+ req = method + " " + url + " HTTP/1.1\r\n"
23
+ req << "Host: homekit\r\n"
24
+
25
+ if type
26
+ req << "Content-Type: " + type + "\r\n"
27
+ end
28
+ if data
29
+ req << "Content-Length: " + data.length.to_s + "\r\n"
30
+ end
31
+ req << "\r\n"
32
+
33
+ if log_debug?
34
+ if data
35
+ if data[0] == '{'
36
+ debug(req + data.to_s)
37
+ else
38
+ debug(req + RubyHome::HAP::TLV.read(data).to_s)
39
+ end
40
+ else
41
+ debug(req)
42
+ end
43
+ end
44
+
45
+ if data
46
+ req << data.to_s
47
+ end
48
+
49
+ if encryption_ready?
50
+ encrypt(req).each do |r|
51
+ if @socket.nil?
52
+ send_data(r)
53
+ else
54
+ @socket.write(r)
55
+ end
56
+ end
57
+ else
58
+ if @socket.nil?
59
+ send_data(req)
60
+ else
61
+ @socket.write(req)
62
+ end
63
+ end
64
+
65
+ init_parser()
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,3 @@
1
+ module HapClient
2
+ VERSION = '0.0.2'
3
+ end
data/lib/hap_client.rb ADDED
@@ -0,0 +1,134 @@
1
+ require 'json'
2
+
3
+ require_relative 'hap_client/log'
4
+ require_relative 'hap_client/parser'
5
+ require_relative 'hap_client/request'
6
+ require_relative 'hap_client/pairing'
7
+
8
+ module HAP
9
+ module Client
10
+ include Log
11
+ include Parser
12
+ include Request
13
+ include Pairing
14
+
15
+ def initialize
16
+ @name = "Unknown Client"
17
+ @mode = :init
18
+ @values = {}
19
+ init_log()
20
+ end
21
+
22
+ def set_value(aid, iid, value)
23
+ info("Set Value #{aid}:#{iid} to #{value}")
24
+ data = {
25
+ "characteristics" => [{
26
+ "aid" => aid,
27
+ "iid" => iid,
28
+ "value" => value
29
+ }]
30
+ }
31
+
32
+ put("/characteristics", "application/hap+json", JSON.generate(data))
33
+ end
34
+
35
+ def subscribe(aid, iid)
36
+ info("Subscribe to #{aid} #{iid}")
37
+ data = {
38
+ "characteristics" => [{
39
+ "aid" => aid,
40
+ "iid" => iid,
41
+ "ev" => "true"
42
+ }]
43
+ }
44
+
45
+ put("/characteristics", "application/hap+json", JSON.generate(data))
46
+ end
47
+
48
+ def subscribe_to_all()
49
+ @values.each do |service|
50
+ service.each do |val|
51
+ if val[:perms].include?("ev")
52
+ subscribe(val[:aid], val[:iid])
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ def get_accessories(&block)
59
+ info("Get Accessories")
60
+ get("/")
61
+
62
+ if block_given?
63
+ @callback = block
64
+ end
65
+ end
66
+
67
+ def to_s
68
+ @name
69
+ end
70
+
71
+ private
72
+
73
+ def parse_message(data)
74
+ case @mode
75
+ when :pair_setup
76
+ pair_setup_parse(data)
77
+ when :pair_verify
78
+ pair_verify_parse(data)
79
+ else
80
+ if !data.nil? and data != ""
81
+ data = parse_accessories(data)
82
+ end
83
+
84
+ if @callback
85
+ t = @callback
86
+ @callback = nil
87
+ t.call(data)
88
+ end
89
+ end
90
+ end
91
+
92
+ def parse_accessories(data)
93
+ data = JSON.parse(data, :symbolize_names=>true)
94
+
95
+ services = data[:accessories][0][:services]
96
+
97
+ services.each do |service|
98
+ @values[service[:type]] = {}
99
+
100
+ parse_characteristics(service)
101
+ end
102
+
103
+ return data
104
+ end
105
+
106
+ def parse_characteristics(service)
107
+ service[:characteristics].each do |char|
108
+ val = char[:value]
109
+
110
+ @values[service[:type]][char[:type]] = {
111
+ :aid => char[:aid],
112
+ :iid => char[:iid],
113
+ :perms => char[:perms},
114
+ :value => val
115
+ }
116
+
117
+ if service[:type] == "3E"
118
+ case char[:type]
119
+ when "20"
120
+ @manufacturer = val
121
+ when "21"
122
+ @model = val
123
+ when "23"
124
+ @name = val
125
+ when "30"
126
+ @serial = val
127
+ when "52"
128
+ @version = val
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
metadata ADDED
@@ -0,0 +1,169 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hap_client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Andreas Bomholtz
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-06-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: eventmachine
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: http_parser.rb
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.6'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: json
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: ruby_home
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '='
60
+ - !ruby/object:Gem::Version
61
+ version: 0.1.2
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '='
67
+ - !ruby/object:Gem::Version
68
+ version: 0.1.2
69
+ - !ruby/object:Gem::Dependency
70
+ name: ruby_home-srp
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '='
74
+ - !ruby/object:Gem::Version
75
+ version: 1.2.0
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '='
81
+ - !ruby/object:Gem::Version
82
+ version: 1.2.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: bundler
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.16'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.16'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '12.3'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '12.3'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '3.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '3.0'
125
+ description: Ruby Gem for Apple Homekit Client
126
+ email: andreas@seluxit.com
127
+ executables: []
128
+ extensions: []
129
+ extra_rdoc_files: []
130
+ files:
131
+ - ".gitignore"
132
+ - ".ruby-version"
133
+ - Gemfile
134
+ - LICENSE
135
+ - README.md
136
+ - Rakefile
137
+ - hap_client.gemspec
138
+ - lib/hap_client.rb
139
+ - lib/hap_client/encryption_request.rb
140
+ - lib/hap_client/log.rb
141
+ - lib/hap_client/pairing.rb
142
+ - lib/hap_client/parser.rb
143
+ - lib/hap_client/request.rb
144
+ - lib/hap_client/version.rb
145
+ homepage: http://github.com/Seluxit/hap_client
146
+ licenses:
147
+ - MIT
148
+ metadata: {}
149
+ post_install_message:
150
+ rdoc_options: []
151
+ require_paths:
152
+ - lib
153
+ required_ruby_version: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ required_rubygems_version: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: '0'
163
+ requirements: []
164
+ rubyforge_project:
165
+ rubygems_version: 2.7.6
166
+ signing_key:
167
+ specification_version: 4
168
+ summary: HAP client
169
+ test_files: []