spotifywebapiPython 1.0.72__py3-none-any.whl → 1.0.73__py3-none-any.whl
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.
Potentially problematic release.
This version of spotifywebapiPython might be problematic. Click here for more details.
- {spotifywebapiPython-1.0.72.dist-info → spotifywebapiPython-1.0.73.dist-info}/METADATA +1 -1
- {spotifywebapiPython-1.0.72.dist-info → spotifywebapiPython-1.0.73.dist-info}/RECORD +25 -25
- spotifywebapipython/const.py +1 -1
- spotifywebapipython/docs/__init__.html +10 -2
- spotifywebapipython/docs/index.html +2 -2
- spotifywebapipython/docs/search.js +1 -1
- spotifywebapipython/docs/spotifywebapiPython/const.html +3 -3
- spotifywebapipython/docs/spotifywebapiPython/saappmessages.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/sautils.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifyapierror.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifyapimessage.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifyauthtoken.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifyclient.html +8 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifydiscovery.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifywebapiauthenticationerror.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifywebapierror.html +2 -2
- spotifywebapipython/models/zeroconfdiscoveryresult.py +13 -0
- spotifywebapipython/spotifyclient.py +64 -19
- spotifywebapipython/zeroconfapi/blobbuilder.py +327 -66
- spotifywebapipython/zeroconfapi/cryptodiffiehellman.py +71 -6
- spotifywebapipython/zeroconfapi/helpers.py +34 -7
- spotifywebapipython/zeroconfapi/zeroconfconnect.py +32 -7
- {spotifywebapiPython-1.0.72.dist-info → spotifywebapiPython-1.0.73.dist-info}/LICENSE +0 -0
- {spotifywebapiPython-1.0.72.dist-info → spotifywebapiPython-1.0.73.dist-info}/WHEEL +0 -0
- {spotifywebapiPython-1.0.72.dist-info → spotifywebapiPython-1.0.73.dist-info}/top_level.txt +0 -0
|
@@ -8,92 +8,353 @@ from base64 import b64decode, b64encode
|
|
|
8
8
|
|
|
9
9
|
from .credentials import Credentials
|
|
10
10
|
from .cryptodiffiehellman import CryptoDiffieHellman
|
|
11
|
-
from .helpers import write_bytes, write_int, byte_list_to_int, b64_to_int, int_to_bytes
|
|
11
|
+
from .helpers import write_bytes, write_int, byte_list_to_int, b64_to_int, int_to_bytes, string_to_int
|
|
12
|
+
|
|
13
|
+
# get smartinspect logger reference; create a new session for this module name.
|
|
14
|
+
from smartinspectpython.siauto import SIAuto, SILevel, SISession, SIMethodParmListContext, SISourceId
|
|
15
|
+
import logging
|
|
16
|
+
_logsi:SISession = SIAuto.Si.GetSession(__name__)
|
|
17
|
+
if (_logsi == None):
|
|
18
|
+
_logsi = SIAuto.Si.AddSession(__name__, True)
|
|
19
|
+
#_logsi.SystemLogger = logging.getLogger(__name__)
|
|
12
20
|
|
|
13
21
|
|
|
14
22
|
class BlobBuilder:
|
|
15
23
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
AES_KEY_SIZE:int = 16 # 16 byte / 128 bit key size for AES encryption
|
|
25
|
+
AES_BLOCK_SIZE:int = 16 # 16 byte / 128 bit block size for AES encryption
|
|
26
|
+
|
|
27
|
+
def __init__(self, credentials:Credentials, device_id:str, remotePublicKeyBase64String:str):
|
|
28
|
+
"""
|
|
29
|
+
Initializes a new instance of the class.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
credentials (Credentials):
|
|
33
|
+
deviceId (str):
|
|
34
|
+
remotePublicKeyBase64String (str):
|
|
35
|
+
Remote public key value in a base64-encoded string format.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# initialize storage.
|
|
39
|
+
self._EncryptedBlob:bytes = b''
|
|
40
|
+
self._EncryptedBlobSigned:bytes = b''
|
|
41
|
+
self._EncryptedBlobSignedBase64String:str = ''
|
|
42
|
+
self._RemotePublicKeyBase64String:str = remotePublicKeyBase64String
|
|
43
|
+
self._RemotePublicKey:int = b64_to_int(remotePublicKeyBase64String)
|
|
44
|
+
|
|
23
45
|
# formulate origin device information.
|
|
24
|
-
self._OriginDeviceName = 'ha-spotifyplus'
|
|
25
|
-
self._OriginDeviceId = hashlib.sha1(bytes(self._OriginDeviceName, 'ascii')).digest().hex()
|
|
46
|
+
self._OriginDeviceName:str = 'ha-spotifyplus'
|
|
47
|
+
self._OriginDeviceId:str = hashlib.sha1(bytes(self._OriginDeviceName, 'ascii')).digest().hex()
|
|
48
|
+
|
|
49
|
+
self.credentials:Credentials = credentials
|
|
50
|
+
self.device_id:bytes = bytes(device_id, 'ascii')
|
|
51
|
+
self.dh_keys = CryptoDiffieHellman()
|
|
52
|
+
self._blob:bytes = b''
|
|
53
|
+
self._decrypted_blob:bytes = b''
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def EncryptedBlob(self) -> bytes:
|
|
58
|
+
"""
|
|
59
|
+
Returns the unsigned encrypted blob as a bytes object.
|
|
60
|
+
"""
|
|
61
|
+
return self._EncryptedBlob
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def EncryptedBlobSigned(self) -> bytes:
|
|
66
|
+
"""
|
|
67
|
+
Returns the signed encrypted blob as a bytes object.
|
|
68
|
+
"""
|
|
69
|
+
return self._EncryptedBlobSigned
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def EncryptedBlobSignedBase64String(self) -> str:
|
|
74
|
+
"""
|
|
75
|
+
Returns the signed encrypted blob as a base64 encoded string.
|
|
76
|
+
"""
|
|
77
|
+
return self._EncryptedBlobSignedBase64String
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def RemotePublicKeyBase64String(self) -> str:
|
|
82
|
+
"""
|
|
83
|
+
Returns the remote public key value in a base64 encoded string format.
|
|
84
|
+
"""
|
|
85
|
+
return self._RemotePublicKeyBase64String
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def RemotePublicKey(self) -> int:
|
|
90
|
+
"""
|
|
91
|
+
Returns the remote public key key value in an integer format.
|
|
92
|
+
"""
|
|
93
|
+
return self._RemotePublicKey
|
|
26
94
|
|
|
27
95
|
|
|
28
96
|
def build(self):
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
97
|
+
"""
|
|
98
|
+
Builds the blob, encrypts it, and returns the signed encrypted blob as a base64 encoded string.
|
|
99
|
+
"""
|
|
100
|
+
self._BuildBlob()
|
|
101
|
+
self._Encrypt()
|
|
102
|
+
return self._EncryptedBlobSignedBase64String
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _BuildBlob(self): # -> bytes
|
|
106
|
+
|
|
107
|
+
apiMethodName:str = 'BlobBuilder._BuildBlob'
|
|
108
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
|
|
112
|
+
# trace.
|
|
113
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
114
|
+
apiMethodParms.AppendKeyValue("device_id", self.device_id)
|
|
115
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Building Spotify Connect ZeroConf addUser Blob", apiMethodParms)
|
|
116
|
+
|
|
117
|
+
# create the blob data that will be sent to the device.
|
|
118
|
+
blob = bytearray()
|
|
119
|
+
# 'I'
|
|
120
|
+
write_int(0x49, blob)
|
|
121
|
+
# username
|
|
122
|
+
write_bytes(self.credentials.username, blob)
|
|
123
|
+
# 'P'
|
|
124
|
+
write_int(0x50, blob)
|
|
125
|
+
# auth_type
|
|
126
|
+
write_int(self.credentials.auth_type, blob)
|
|
127
|
+
# 'Q'
|
|
128
|
+
write_int(0x51, blob)
|
|
129
|
+
# password
|
|
130
|
+
write_bytes(self.credentials.password, blob)
|
|
131
|
+
_logsi.LogBinary(SILevel.Verbose, "Blob Data, Unencrypted Bytes", blob)
|
|
132
|
+
# padding
|
|
133
|
+
n_zeros = BlobBuilder.AES_BLOCK_SIZE - (len(blob) % BlobBuilder.AES_BLOCK_SIZE) - 1
|
|
134
|
+
blob.extend([0] * n_zeros)
|
|
135
|
+
blob.append(n_zeros + 1)
|
|
136
|
+
_logsi.LogBinary(SILevel.Verbose, "Blob Data, Unencrypted Bytes (with padding)", blob)
|
|
137
|
+
|
|
138
|
+
# not sure what the following is really trying to accomplish.
|
|
139
|
+
# it leaves the first 16 bytes as-is, then starts performing a Bitwise XOR on
|
|
140
|
+
# on each byte remaining in the buffer (starting at offset 0x11 / byte 17).
|
|
141
|
+
blen = len(blob)
|
|
142
|
+
for i in range(blen - 0x11, -1, -1):
|
|
143
|
+
blob[blen - i - 1] ^= blob[blen - i - 0x11]
|
|
144
|
+
_logsi.LogBinary(SILevel.Verbose, "Blob Data, Unencrypted Bytes after XOR of bytes 16 thru %d" % (blen), blob)
|
|
56
145
|
|
|
57
|
-
|
|
146
|
+
# secret is the deviceId value.
|
|
147
|
+
secret = hashlib.sha1(self.device_id).digest()
|
|
148
|
+
_logsi.LogBinary(SILevel.Verbose, "Blob Data Secret, SHA1 Digest", secret)
|
|
58
149
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
150
|
+
# create a unique, strong hash for the given secret.
|
|
151
|
+
keys = PBKDF2(secret, self.credentials.username, 20, count=0x100, hmac_hash_module=SHA1)
|
|
152
|
+
key = bytearray(hashlib.sha1(keys).digest()[:20])
|
|
153
|
+
key.extend(bytearray([0x00, 0x00, 0x00, 0x14]))
|
|
154
|
+
_logsi.LogBinary(SILevel.Verbose, "Blob Data Encryption Key", key)
|
|
62
155
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
156
|
+
# create a new AES cipher that will be used to encrypt the data.
|
|
157
|
+
# use the hashed secret as the encryption key.
|
|
158
|
+
# data will be encrypted in 16-byte blocks (or chunks).
|
|
159
|
+
encrypted_blob = bytearray()
|
|
160
|
+
cipher = AES.new(key, mode=AES.MODE_ECB)
|
|
66
161
|
|
|
67
|
-
|
|
68
|
-
|
|
162
|
+
def chunker(seq, size):
|
|
163
|
+
return (seq[pos:pos + size] for pos in range(0, len(seq), size))
|
|
69
164
|
|
|
70
|
-
|
|
71
|
-
|
|
165
|
+
# encrypt the data.
|
|
166
|
+
for chunk in chunker(blob, BlobBuilder.AES_BLOCK_SIZE):
|
|
167
|
+
encrypted_blob.extend(cipher.encrypt(chunk))
|
|
168
|
+
|
|
169
|
+
# trace.
|
|
170
|
+
_logsi.LogBinary(SILevel.Verbose, "Blob Data, Encrypted Bytes", encrypted_blob)
|
|
72
171
|
|
|
73
|
-
|
|
172
|
+
# convert encrypted bytes to a base64-encoded string so we can
|
|
173
|
+
# send it to the device via an http request.
|
|
174
|
+
self._blob = b64encode(encrypted_blob)
|
|
175
|
+
_logsi.LogBinary(SILevel.Verbose, "Blob Data, Encrypted base64-encoded bytes", self._blob)
|
|
74
176
|
|
|
177
|
+
finally:
|
|
178
|
+
|
|
179
|
+
# trace.
|
|
180
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
75
181
|
|
|
76
|
-
def _encrypt(self):
|
|
77
|
-
remote_device_key = b64_to_int(self.remote_pubkey)
|
|
78
|
-
shared_key = self.dh_keys.shared_secret(remote_device_key)
|
|
79
182
|
|
|
80
|
-
|
|
81
|
-
|
|
183
|
+
def _GetIVectorAES128(self) -> bytes:
|
|
184
|
+
"""
|
|
185
|
+
Returns a new initialization vector for AES (128-bit) cryptographic functions.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
A new initialization vector of 16 bytes (128-bits).</returns>
|
|
189
|
+
"""
|
|
190
|
+
iv_data:list = [253, 81, 222, 19, 70, 203, 45, 89, 141, 68, 210, 240, 93, 20, 76, 30]
|
|
191
|
+
return int.from_bytes(bytes(iv_data), 'big')
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _Encrypt(self):
|
|
195
|
+
"""
|
|
196
|
+
Encrypts the `_blob` argument and
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
apiMethodName:str = 'BlobBuilder.Encrypt'
|
|
200
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
|
|
204
|
+
# trace.
|
|
205
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
206
|
+
apiMethodParms.AppendKeyValue("RemotePublicKeyBase64String", self.RemotePublicKeyBase64String)
|
|
207
|
+
apiMethodParms.AppendKeyValue("RemotePublicKey", self.RemotePublicKey)
|
|
208
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Encrypt Spotify Connect ZeroConf addUser Blob", apiMethodParms)
|
|
209
|
+
|
|
210
|
+
# trace.
|
|
211
|
+
_logsi.LogInt(SILevel.Verbose, "Remote Public Key: %s" % self.RemotePublicKey, self.RemotePublicKey)
|
|
212
|
+
|
|
213
|
+
# create shared key.
|
|
214
|
+
shared_key:int = self.dh_keys.SharedSecret(self.RemotePublicKey)
|
|
215
|
+
_logsi.LogInt(SILevel.Verbose, "Shared Key: %s" % shared_key, shared_key)
|
|
216
|
+
|
|
217
|
+
b_shared_key:bytes = int_to_bytes(shared_key)
|
|
218
|
+
_logsi.LogBinary(SILevel.Verbose, "Shared Key bytes", b_shared_key)
|
|
219
|
+
base_key:bytes = hashlib.sha1(b_shared_key).digest()[:16]
|
|
220
|
+
_logsi.LogBinary(SILevel.Verbose, "Base Key bytes", base_key)
|
|
82
221
|
|
|
83
|
-
|
|
84
|
-
|
|
222
|
+
# create encryption key (16 byte / 128 bit key size for AES encryption).
|
|
223
|
+
encryption_key:bytes = hmac.new(base_key, b'encryption', 'sha1').digest()
|
|
224
|
+
encryption_key = encryption_key[:BlobBuilder.AES_KEY_SIZE]
|
|
225
|
+
_logsi.LogBinary(SILevel.Verbose, "Encryption Key (AES128-bit) bytes", encryption_key)
|
|
85
226
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
227
|
+
# create new AES encryption cipher, and encrypt the data.
|
|
228
|
+
iv:list[int] = [253, 81, 222, 19, 70, 203, 45, 89, 141, 68, 210, 240, 93, 20, 76, 30]
|
|
229
|
+
ctr_e:dict = Counter.new(128, initial_value=self._GetIVectorAES128())
|
|
230
|
+
cipher = AES.new(encryption_key, AES.MODE_CTR, counter=ctr_e)
|
|
231
|
+
encrypted_blob:bytes = cipher.encrypt(self._blob)
|
|
232
|
+
_logsi.LogBinary(SILevel.Verbose, "Encrypted Blob (unsigned, bytes)", encrypted_blob)
|
|
90
233
|
|
|
91
|
-
|
|
234
|
+
# create checksum key and checksum value of encrypted data.
|
|
235
|
+
checksum_key:bytes = hmac.new(base_key, b'checksum', 'sha1').digest()
|
|
236
|
+
_logsi.LogBinary(SILevel.Verbose, "Checksum Key: %s" % checksum_key.hex(), checksum_key)
|
|
237
|
+
checksum:bytes = hmac.new(checksum_key, encrypted_blob, 'sha1').digest()
|
|
238
|
+
_logsi.LogBinary(SILevel.Verbose, "Checksum Value (bytes)", checksum)
|
|
92
239
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
240
|
+
# create signed encrypted data buffer that contains the encrypted data and checksum.
|
|
241
|
+
self._EncryptedBlobSigned:bytearray = bytearray(iv)
|
|
242
|
+
self._EncryptedBlobSigned.extend(encrypted_blob)
|
|
243
|
+
self._EncryptedBlobSigned.extend(checksum)
|
|
244
|
+
_logsi.LogBinary(SILevel.Verbose, "Encrypted Blob (signed with checksum, bytes)", self._EncryptedBlobSigned)
|
|
96
245
|
|
|
97
|
-
|
|
246
|
+
# convert encrypted bytes to a base64-encoded string.
|
|
247
|
+
b64result_bytes:bytes = b64encode(self._EncryptedBlobSigned)
|
|
248
|
+
self._EncryptedBlobSignedBase64String:str = b64result_bytes.decode('ascii')
|
|
249
|
+
_logsi.LogText(SILevel.Verbose, "Encrypted Blob (signed with checksum, base64-encoded string)", self._EncryptedBlobSignedBase64String)
|
|
98
250
|
|
|
99
|
-
|
|
251
|
+
finally:
|
|
252
|
+
|
|
253
|
+
# trace.
|
|
254
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# def _Decrypt(
|
|
258
|
+
# self,
|
|
259
|
+
# encrypted_blob_signed:str,
|
|
260
|
+
# ) -> None:
|
|
261
|
+
# """
|
|
262
|
+
# Decreypts the specified blob that was previously encrypted and signed by the
|
|
263
|
+
# `Encrypt` method.
|
|
264
|
+
|
|
265
|
+
# Args:
|
|
266
|
+
# encrypted_blob_signed (str):
|
|
267
|
+
# A base64 encoded string that represents a signed and encrypted data blob; the
|
|
268
|
+
# packet consists of the IV, data, and checksum blocks.
|
|
269
|
+
# """
|
|
270
|
+
|
|
271
|
+
# apiMethodName:str = 'BlobBuilder.Decrypt'
|
|
272
|
+
# apiMethodParms:SIMethodParmListContext = None
|
|
273
|
+
|
|
274
|
+
# try:
|
|
275
|
+
|
|
276
|
+
# # trace.
|
|
277
|
+
# apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
278
|
+
# apiMethodParms.AppendKeyValue("encrypted_blob_signed", encrypted_blob_signed)
|
|
279
|
+
# apiMethodParms.AppendKeyValue("RemotePublicKeyBase64String", self.RemotePublicKeyBase64String)
|
|
280
|
+
# _logsi.LogMethodParmList(SILevel.Verbose, "Decrypt Spotify Connect ZeroConf addUser Blob", apiMethodParms)
|
|
281
|
+
|
|
282
|
+
# if encrypted_blob_signed is not None:
|
|
283
|
+
# self._EncryptedBlobSignedBase64String = encrypted_blob_signed
|
|
284
|
+
|
|
285
|
+
# # convert base64-encoded string to bytes.
|
|
286
|
+
# _logsi.LogText(SILevel.Verbose, "Encrypted Blob (signed with checksum, base64-encoded string)", self._EncryptedBlobSignedBase64String)
|
|
287
|
+
# self._EncryptedBlobSigned:bytes = b64decode(self._EncryptedBlobSignedBase64String)
|
|
288
|
+
# _logsi.LogBinary(SILevel.Verbose, "Encrypted Blob (signed with checksum, bytes)", self._EncryptedBlobSigned)
|
|
289
|
+
|
|
290
|
+
# # get the individual components of the signed blob packet.
|
|
291
|
+
# iv = self._EncryptedBlobSigned[:16]
|
|
292
|
+
# self._EncryptedBlob = self._EncryptedBlobSigned[16:len(self._EncryptedBlobSigned)-20]
|
|
293
|
+
# checksum = self._EncryptedBlobSigned[len(self._EncryptedBlobSigned)-20:]
|
|
294
|
+
# _logsi.LogBinary(SILevel.Verbose, "Checksum Value (bytes)", checksum)
|
|
295
|
+
# _logsi.LogBinary(SILevel.Verbose, "Encrypted Blob (unsigned, bytes)", self._EncryptedBlob)
|
|
296
|
+
|
|
297
|
+
# clientKey:str = "7efznlz1HspR5tQftOUdz7cv/L6V5EuXWI596GyiQMist7piBWxfbdAjcpP9/o7WnCl5HdwgI7nY6cQ7ktz3Tsbs4kSG5mbi1QwnYi8IbJ9sdYdd8YxI3Vl6dfuG3JWP"
|
|
298
|
+
# result_b64decode = b64decode(clientKey)
|
|
299
|
+
# result_b64_to_int = b64_to_int(clientKey) # 114863 ...
|
|
300
|
+
# # result_string_to_int = string_to_int(clientKey) # 114863 ...
|
|
301
|
+
# # result_byte_list_to_int = byte_list_to_int(clientKey) # 114863 ...
|
|
302
|
+
# # result_dh_keys_Exchange = self.dh_keys.Exchange(clientKey) # 6243 ...
|
|
303
|
+
# #resultInt = self.dh_keys.Exchange(clientKey) # exception
|
|
304
|
+
# #resultInt = self.dh_keys.shared_secret(b64_to_int(resultBytes)) # exception
|
|
305
|
+
# #result_dh_keys_shared_secret = self.dh_keys.shared_secret(string_to_int(resultBytes)) # 438506 ...
|
|
306
|
+
# #resultInt = self.dh_keys.shared_secret(string_to_int(clientKey)) # exception ...
|
|
307
|
+
|
|
308
|
+
# remote_device_key:int = b64_to_int(clientKey)
|
|
309
|
+
# shared_key:int = 478549389067009439856765960469867849375843534534536997108028584308833390603433510828238916782187762759272369310194601566745030716193055009703822032238289108648174583850368649722797922715533666774901465941959787614689524297840942749
|
|
310
|
+
# shared_key_int_to_bytes = int_to_bytes(shared_key)
|
|
311
|
+
# #shared_key_b64encode = b64encode(shared_key)
|
|
312
|
+
# shared_key_b64encode_int_to_bytes = b64encode(int_to_bytes(shared_key))
|
|
313
|
+
# shared_key_b64encode_utf8 = b64encode(int_to_bytes(shared_key)).decode('utf-8')
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# #shared_key:int = self.dh_keys.shared_secret(remote_device_key)
|
|
317
|
+
# b_shared_key:bytes = int_to_bytes(shared_key)
|
|
318
|
+
# base_key:bytes = hashlib.sha1(b_shared_key).digest()[:16]
|
|
319
|
+
# #tmpbytes = b64decode(clientKey)
|
|
320
|
+
# #shared_key:int = byte_list_to_int(tmpbytes)
|
|
321
|
+
# #shared_key:int = b64_to_int(clientKey)
|
|
322
|
+
# #shared_key = self.dh_keys.Exchange(clientKey)
|
|
323
|
+
# _logsi.LogValue(SILevel.Verbose, "Shared Key: %s" % shared_key, shared_key)
|
|
324
|
+
# #baseKey := func() []byte { sum := sha1.Sum(sharedSecret); return sum[:16] }()
|
|
325
|
+
# #b_shared_key:bytes = int_to_bytes(shared_key)
|
|
326
|
+
# #base_key:bytes = hashlib.sha1(b_shared_key).digest()[:16]
|
|
327
|
+
# #encryptionKey := func() []byte { sum := mac.Sum(nil); return sum[:16] }()
|
|
328
|
+
# encryption_key:bytes = hmac.new(base_key, b'encryption', 'sha1').digest()
|
|
329
|
+
# _logsi.LogBinary(SILevel.Verbose, "Encryption Key", encryption_key)
|
|
330
|
+
|
|
331
|
+
# # # create remote device key.
|
|
332
|
+
# # remote_device_key:int = b64_to_int(self.RemotePublicKeyBase64String)
|
|
333
|
+
# # _logsi.LogValue(SILevel.Verbose, "Remote Device Key", remote_device_key)
|
|
334
|
+
|
|
335
|
+
# # # create shared key.
|
|
336
|
+
# # shared_key:int = self.dh_keys.shared_secret(remote_device_key)
|
|
337
|
+
# # _logsi.LogValue(SILevel.Verbose, "Shared Key: %s" % shared_key, shared_key)
|
|
338
|
+
|
|
339
|
+
# # b_shared_key:bytes = int_to_bytes(shared_key)
|
|
340
|
+
# # base_key:bytes = hashlib.sha1(b_shared_key).digest()[:16]
|
|
341
|
+
|
|
342
|
+
# # # create encryption key.
|
|
343
|
+
# # encryption_key:bytes = hmac.new(base_key, b'encryption', 'sha1').digest()
|
|
344
|
+
# # _logsi.LogBinary(SILevel.Verbose, "Encryption Key", encryption_key)
|
|
345
|
+
|
|
346
|
+
# # create new AES cipher, and decrypt the data.
|
|
347
|
+
# ctr_e:dict = Counter.new(128, initial_value=byte_list_to_int(iv))
|
|
348
|
+
# cipher = AES.new(encryption_key[:16], AES.MODE_CTR, counter=ctr_e)
|
|
349
|
+
# self._decrypted_blob = cipher.decrypt(self._EncryptedBlob)
|
|
350
|
+
# _logsi.LogBinary(SILevel.Verbose, "Blob Data, Encrypted base64-encoded bytes", self._decrypted_blob)
|
|
351
|
+
# self._blob = b64decode(self._decrypted_blob)
|
|
352
|
+
# _logsi.LogBinary(SILevel.Verbose, "Blob Data, Encrypted Bytes", self._blob)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# # cipher.NewCTR(bc, iv).XORKeyStream(decrypted, encrypted)
|
|
356
|
+
|
|
357
|
+
# finally:
|
|
358
|
+
|
|
359
|
+
# # trace.
|
|
360
|
+
# _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import secrets
|
|
2
2
|
|
|
3
|
-
from .helpers import byte_list_to_int
|
|
3
|
+
from .helpers import byte_list_to_int, int_to_bytes, b64_to_int, int_to_b64str
|
|
4
4
|
|
|
5
5
|
class CryptoDiffieHellman:
|
|
6
|
+
"""
|
|
7
|
+
Diffie Hellman cryptography helper class.
|
|
8
|
+
|
|
9
|
+
The following link does a pretty good job of explaining this process:
|
|
10
|
+
https://www.comparitech.com/blog/information-security/diffie-hellman-key-exchange/
|
|
11
|
+
"""
|
|
6
12
|
|
|
7
13
|
DH_GENERATOR = 2
|
|
8
|
-
|
|
14
|
+
DH_PRIME_MODULUS = byte_list_to_int([
|
|
9
15
|
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2,
|
|
10
16
|
0x34, 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67,
|
|
11
17
|
0xcc, 0x74, 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e,
|
|
@@ -16,9 +22,68 @@ class CryptoDiffieHellman:
|
|
|
16
22
|
|
|
17
23
|
|
|
18
24
|
def __init__(self):
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
"""
|
|
26
|
+
Initializes a new instance of the class.
|
|
27
|
+
"""
|
|
28
|
+
# generate our private key value.
|
|
29
|
+
self._PrivateKey = secrets.randbits(95 * 8)
|
|
30
|
+
|
|
31
|
+
# generate a public key that we can share with a partner.
|
|
32
|
+
self._PublicKey:int = pow(CryptoDiffieHellman.DH_GENERATOR, self._PrivateKey, CryptoDiffieHellman.DH_PRIME_MODULUS)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def PublicKey(self) -> int:
|
|
37
|
+
"""
|
|
38
|
+
Returns our public key in an integer format.
|
|
39
|
+
"""
|
|
40
|
+
return self._PublicKey
|
|
41
|
+
|
|
42
|
+
@PublicKey.setter
|
|
43
|
+
def PublicKey(self, value:int):
|
|
44
|
+
"""
|
|
45
|
+
Sets the PublicKey property value.
|
|
46
|
+
"""
|
|
47
|
+
if isinstance(value, int):
|
|
48
|
+
self._PublicKey = value
|
|
49
|
+
|
|
21
50
|
|
|
51
|
+
@property
|
|
52
|
+
def PublicKeyBase64String(self) -> str:
|
|
53
|
+
"""
|
|
54
|
+
Returns our public key in a base64 encoded string format.
|
|
55
|
+
"""
|
|
56
|
+
return int_to_b64str(self._PublicKey)
|
|
57
|
+
|
|
22
58
|
|
|
23
|
-
def
|
|
24
|
-
|
|
59
|
+
def SharedSecret(self, remotePublicKey:int):
|
|
60
|
+
"""
|
|
61
|
+
Returns a shared secret value that is calculated based upon the specified `remotePublicKey`
|
|
62
|
+
value, our private key value, and a shared Modulus value.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
remotePublicKey (int):
|
|
66
|
+
The Remote clients PublicKey value.
|
|
67
|
+
|
|
68
|
+
The shared secret value can be calculated as:
|
|
69
|
+
SharedSecret = remotePublicKey(base,g) ** PrivateKey(secret,a) % DH_PRIME(modulus,p)
|
|
70
|
+
SharedSecret = g**a mod p
|
|
71
|
+
SharedSecret = base ** secret % modulus
|
|
72
|
+
|
|
73
|
+
- base = remotePublicKey
|
|
74
|
+
- secret = PrivateKey
|
|
75
|
+
- modulus = DH_PRIME
|
|
76
|
+
"""
|
|
77
|
+
# calculate the shared secret value based upon the remote's PublicKey, our PrivateKey, and the shared Modulus.
|
|
78
|
+
|
|
79
|
+
# another way to express this formula is:
|
|
80
|
+
# SharedSecret = g**a mod p
|
|
81
|
+
|
|
82
|
+
# another way to express this formula is:
|
|
83
|
+
# SharedSecret = base ** secret % modulus
|
|
84
|
+
|
|
85
|
+
# - base or `g` = remotePublicKey
|
|
86
|
+
# - secret or `a` = PrivateKey
|
|
87
|
+
# - modulus or `p` = DH_PRIME
|
|
88
|
+
return pow(remotePublicKey, self._PrivateKey, CryptoDiffieHellman.DH_PRIME_MODULUS)
|
|
89
|
+
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from base64 import b64decode, b64encode
|
|
2
|
+
import binascii
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
def write_int(i: int, out: bytearray):
|
|
@@ -14,17 +15,43 @@ def write_bytes(b: bytes, out: bytearray):
|
|
|
14
15
|
out.extend(b)
|
|
15
16
|
|
|
16
17
|
|
|
17
|
-
def byte_list_to_int(byte_list):
|
|
18
|
+
def byte_list_to_int(byte_list) -> int:
|
|
19
|
+
"""
|
|
20
|
+
Converts a list of bytes to a big integer.
|
|
21
|
+
"""
|
|
18
22
|
return int.from_bytes(bytes(byte_list), 'big')
|
|
19
23
|
|
|
20
24
|
|
|
21
|
-
def int_to_bytes(
|
|
22
|
-
|
|
25
|
+
def int_to_bytes(value: int) -> bytes:
|
|
26
|
+
"""
|
|
27
|
+
Converts a big integer to bytes.
|
|
28
|
+
"""
|
|
29
|
+
return value.to_bytes((value.bit_length() + 7) // 8, byteorder='big') or b'\0'
|
|
23
30
|
|
|
24
31
|
|
|
25
|
-
def int_to_b64str(
|
|
26
|
-
|
|
32
|
+
def int_to_b64str(value: int) -> str:
|
|
33
|
+
"""
|
|
34
|
+
Converts a big integer to a base64-encoded string value.
|
|
35
|
+
"""
|
|
36
|
+
return b64encode(int_to_bytes(value)).decode('ascii')
|
|
27
37
|
|
|
28
38
|
|
|
29
|
-
def b64_to_int(
|
|
30
|
-
|
|
39
|
+
def b64_to_int(value: str):
|
|
40
|
+
"""
|
|
41
|
+
Converts a base64-encoded string or byte array to a big integer.
|
|
42
|
+
"""
|
|
43
|
+
return int.from_bytes(b64decode(value), 'big')
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def string_to_int(value):
|
|
47
|
+
"""
|
|
48
|
+
Converts a string to a big integer.
|
|
49
|
+
"""
|
|
50
|
+
return int(binascii.hexlify(value), 16)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def int_of_string(value):
|
|
54
|
+
"""
|
|
55
|
+
Converts a string to a big integer.
|
|
56
|
+
"""
|
|
57
|
+
return int(binascii.hexlify(value), 16)
|
|
@@ -462,6 +462,8 @@ class ZeroconfConnect:
|
|
|
462
462
|
apiMethodParms.AppendKeyValue("includeOriginDeviceInfo", includeOriginDeviceInfo)
|
|
463
463
|
apiMethodParms.AppendKeyValue("info.Availability", info.Availability)
|
|
464
464
|
apiMethodParms.AppendKeyValue("info.PublicKey", info.PublicKey)
|
|
465
|
+
apiMethodParms.AppendKeyValue("info.DeviceId", info.DeviceId)
|
|
466
|
+
apiMethodParms.AppendKeyValue("info.RemoteName", info.RemoteName)
|
|
465
467
|
_logsi.LogMethodParmList(SILevel.Verbose, "Issuing Spotify Connect Zeroconf addUser request (ip=%s)" % self._HostIpAddress, apiMethodParms)
|
|
466
468
|
|
|
467
469
|
# validations.
|
|
@@ -485,6 +487,7 @@ class ZeroconfConnect:
|
|
|
485
487
|
|
|
486
488
|
# set tokenType based on getInfo token type.
|
|
487
489
|
# if TokenType is 'athorization_code', then use it as-is;
|
|
490
|
+
# if TokenType is 'accesstoken', then use `default`.
|
|
488
491
|
# otherwise, use 'default'.
|
|
489
492
|
tokenType:str = 'default'
|
|
490
493
|
if info.TokenType == 'authorization_code':
|
|
@@ -493,7 +496,7 @@ class ZeroconfConnect:
|
|
|
493
496
|
# set clientKey based on getInfo token type.
|
|
494
497
|
# if TokenType is 'athorization_code', then clientKey should be null.
|
|
495
498
|
# otherwise, clientKey should be the PublicKey (base64 encoded).
|
|
496
|
-
clientKey:str =
|
|
499
|
+
clientKey:str = builder.dh_keys.PublicKeyBase64String
|
|
497
500
|
if info.TokenType == 'authorization_code':
|
|
498
501
|
clientKey = ''
|
|
499
502
|
|
|
@@ -502,9 +505,6 @@ class ZeroconfConnect:
|
|
|
502
505
|
'action': 'addUser',
|
|
503
506
|
'version': info.Version,
|
|
504
507
|
'tokenType': tokenType,
|
|
505
|
-
#'version': self.Version,
|
|
506
|
-
#'tokenType': 'default', # NOTE - not the same as info.TokenType!
|
|
507
|
-
#'clientKey': int_to_b64str(builder.dh_keys.public_key),
|
|
508
508
|
'clientKey': clientKey,
|
|
509
509
|
'loginId': loginId or '', # canonical login id (e.g. "31l77fd87g8h9j00k89f07jf87ge")
|
|
510
510
|
'userName': credentials.username.decode('ascii'), # user name (e.g. "youremail@mail.com")
|
|
@@ -643,9 +643,31 @@ class ZeroconfConnect:
|
|
|
643
643
|
# trace.
|
|
644
644
|
_logsi.LogObject(SILevel.Verbose, '%s result (%s)' % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
|
|
645
645
|
|
|
646
|
-
#
|
|
646
|
+
# was a status value returned?
|
|
647
|
+
# it's been found that some devices (Sonos, etc) do not return a proper JSON response.
|
|
648
|
+
# for these cases, we will rely on the http status code for the response code.
|
|
649
|
+
if (result.Status is None):
|
|
650
|
+
|
|
651
|
+
# was a good http status code returned?
|
|
652
|
+
if response.status_code == 200:
|
|
653
|
+
# yes - default status results if they are not set.
|
|
654
|
+
result.Status = 101
|
|
655
|
+
if result.StatusString is None:
|
|
656
|
+
result.StatusString = "OK"
|
|
657
|
+
if result.SpotifyError is None:
|
|
658
|
+
result.SpotifyError = 0
|
|
659
|
+
|
|
660
|
+
# are we processing result status?
|
|
647
661
|
if (not ignoreStatusResult):
|
|
648
|
-
|
|
662
|
+
|
|
663
|
+
if (result.Status is None) and (response.status_code != 200):
|
|
664
|
+
# if status value was not returned and http status is not 200, then
|
|
665
|
+
# raise an exception with http status details.
|
|
666
|
+
raise SpotifyZeroconfApiError(response.status_code, responseData, apiMethodName, response.reason, _logsi)
|
|
667
|
+
|
|
668
|
+
elif (result.Status != 101):
|
|
669
|
+
# if status was returned and it's not ok, then raise an exception
|
|
670
|
+
# with JSON response details.
|
|
649
671
|
raise SpotifyZeroconfApiError(result.Status, result.ToString(), apiMethodName, result.StatusString, _logsi)
|
|
650
672
|
|
|
651
673
|
# give spotify zeroconf api time to process the change.
|
|
@@ -724,6 +746,9 @@ class ZeroconfConnect:
|
|
|
724
746
|
Raises:
|
|
725
747
|
SpotifyWebApiError:
|
|
726
748
|
If the Spotify Zeroconf API request response contains error information.
|
|
749
|
+
|
|
750
|
+
The request will timeout after 5 seconds and an exception raised if the device cannot be reached
|
|
751
|
+
or does not respond within that time frame.
|
|
727
752
|
|
|
728
753
|
<details>
|
|
729
754
|
<summary>Sample Code</summary>
|
|
@@ -768,7 +793,7 @@ class ZeroconfConnect:
|
|
|
768
793
|
# execute spotify zeroconf api request.
|
|
769
794
|
response = requests.get(
|
|
770
795
|
self._Uri,
|
|
771
|
-
timeout=
|
|
796
|
+
timeout=5,
|
|
772
797
|
headers=reqHeaders,
|
|
773
798
|
params=reqParams
|
|
774
799
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|