spotifywebapiPython 1.0.242__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.
Files changed (138) hide show
  1. spotifywebapipython/__init__.py +35 -0
  2. spotifywebapipython/const.py +114 -0
  3. spotifywebapipython/docs/__init__.html +2156 -0
  4. spotifywebapipython/docs/codecopy.js +70 -0
  5. spotifywebapipython/docs/index.html +514 -0
  6. spotifywebapipython/docs/search.js +46 -0
  7. spotifywebapipython/docs/spotifywebapipython/const.html +636 -0
  8. spotifywebapipython/docs/spotifywebapipython/saappmessages.html +540 -0
  9. spotifywebapipython/docs/spotifywebapipython/sautils.html +620 -0
  10. spotifywebapipython/docs/spotifywebapipython/spotifyapierror.html +341 -0
  11. spotifywebapipython/docs/spotifywebapipython/spotifyapimessage.html +598 -0
  12. spotifywebapipython/docs/spotifywebapipython/spotifyauthtoken.html +612 -0
  13. spotifywebapipython/docs/spotifywebapipython/spotifyclient.html +16557 -0
  14. spotifywebapipython/docs/spotifywebapipython/spotifydiscovery.html +518 -0
  15. spotifywebapipython/docs/spotifywebapipython/spotifywebapiauthenticationerror.html +421 -0
  16. spotifywebapipython/docs/spotifywebapipython/spotifywebapierror.html +400 -0
  17. spotifywebapipython/docs/spotifywebapipython/spotifywebplayertoken.html +635 -0
  18. spotifywebapipython/docs/spotifywebapipython.ico +0 -0
  19. spotifywebapipython/docs/spotifywebapipython_OLD.ico +0 -0
  20. spotifywebapipython/models/__init__.py +121 -0
  21. spotifywebapipython/models/album.py +170 -0
  22. spotifywebapipython/models/albumpagesaved.py +85 -0
  23. spotifywebapipython/models/albumpagesimplified.py +88 -0
  24. spotifywebapipython/models/albumsaved.py +102 -0
  25. spotifywebapipython/models/albumsimplified.py +308 -0
  26. spotifywebapipython/models/artist.py +146 -0
  27. spotifywebapipython/models/artistinfo.py +282 -0
  28. spotifywebapipython/models/artistinfotourevent.py +127 -0
  29. spotifywebapipython/models/artistpage.py +70 -0
  30. spotifywebapipython/models/artistsimplified.py +163 -0
  31. spotifywebapipython/models/audiobook.py +87 -0
  32. spotifywebapipython/models/audiobookpagesimplified.py +88 -0
  33. spotifywebapipython/models/audiobooksimplified.py +362 -0
  34. spotifywebapipython/models/audiofeatures.py +333 -0
  35. spotifywebapipython/models/author.py +67 -0
  36. spotifywebapipython/models/category.py +168 -0
  37. spotifywebapipython/models/categorypage.py +70 -0
  38. spotifywebapipython/models/chapter.py +84 -0
  39. spotifywebapipython/models/chapterpagesimplified.py +88 -0
  40. spotifywebapipython/models/chaptersimplified.py +387 -0
  41. spotifywebapipython/models/context.py +113 -0
  42. spotifywebapipython/models/copyright.py +79 -0
  43. spotifywebapipython/models/device.py +301 -0
  44. spotifywebapipython/models/episode.py +84 -0
  45. spotifywebapipython/models/episodepagesaved.py +85 -0
  46. spotifywebapipython/models/episodepagesimplified.py +88 -0
  47. spotifywebapipython/models/episodesaved.py +99 -0
  48. spotifywebapipython/models/episodesimplified.py +370 -0
  49. spotifywebapipython/models/explicitcontent.py +80 -0
  50. spotifywebapipython/models/externalids.py +91 -0
  51. spotifywebapipython/models/externalurls.py +104 -0
  52. spotifywebapipython/models/followers.py +81 -0
  53. spotifywebapipython/models/imageobject.py +193 -0
  54. spotifywebapipython/models/imagevibrantcolors.py +182 -0
  55. spotifywebapipython/models/linkedfrom.py +165 -0
  56. spotifywebapipython/models/narrator.py +67 -0
  57. spotifywebapipython/models/owner.py +162 -0
  58. spotifywebapipython/models/pageobject.py +343 -0
  59. spotifywebapipython/models/playeractions.py +196 -0
  60. spotifywebapipython/models/playerlastplayedinfo.py +334 -0
  61. spotifywebapipython/models/playerplaystate.py +467 -0
  62. spotifywebapipython/models/playerqueueinfo.py +207 -0
  63. spotifywebapipython/models/playhistory.py +136 -0
  64. spotifywebapipython/models/playhistorypage.py +85 -0
  65. spotifywebapipython/models/playlist.py +125 -0
  66. spotifywebapipython/models/playlistpage.py +85 -0
  67. spotifywebapipython/models/playlistpagesimplified.py +107 -0
  68. spotifywebapipython/models/playlistsimplified.py +334 -0
  69. spotifywebapipython/models/playlisttrack.py +143 -0
  70. spotifywebapipython/models/playlisttracksummary.py +80 -0
  71. spotifywebapipython/models/recommendationseed.py +135 -0
  72. spotifywebapipython/models/restrictions.py +86 -0
  73. spotifywebapipython/models/resumepoint.py +79 -0
  74. spotifywebapipython/models/searchresponse.py +391 -0
  75. spotifywebapipython/models/searchresultbase.py +299 -0
  76. spotifywebapipython/models/show.py +87 -0
  77. spotifywebapipython/models/showpagesaved.py +85 -0
  78. spotifywebapipython/models/showpagesimplified.py +88 -0
  79. spotifywebapipython/models/showsaved.py +99 -0
  80. spotifywebapipython/models/showsimplified.py +320 -0
  81. spotifywebapipython/models/spotifyconnectdevice.py +321 -0
  82. spotifywebapipython/models/spotifyconnectdevices.py +441 -0
  83. spotifywebapipython/models/track.py +142 -0
  84. spotifywebapipython/models/trackpage.py +110 -0
  85. spotifywebapipython/models/trackpagesaved.py +85 -0
  86. spotifywebapipython/models/trackpagesimplified.py +88 -0
  87. spotifywebapipython/models/trackrecommendations.py +88 -0
  88. spotifywebapipython/models/tracksaved.py +99 -0
  89. spotifywebapipython/models/tracksimplified.py +398 -0
  90. spotifywebapipython/models/userprofile.py +184 -0
  91. spotifywebapipython/models/userprofilesimplified.py +206 -0
  92. spotifywebapipython/models/zeroconfdiscoveryresult.py +592 -0
  93. spotifywebapipython/models/zeroconfproperty.py +93 -0
  94. spotifywebapipython/oauthcli/__init__.py +7 -0
  95. spotifywebapipython/oauthcli/authclient.py +1229 -0
  96. spotifywebapipython/saappmessages.py +86 -0
  97. spotifywebapipython/sautils.py +285 -0
  98. spotifywebapipython/spotifyapierror.py +66 -0
  99. spotifywebapipython/spotifyapimessage.py +269 -0
  100. spotifywebapipython/spotifyauthtoken.py +264 -0
  101. spotifywebapipython/spotifyclient.py +17885 -0
  102. spotifywebapipython/spotifyconnect/__init__.py +19 -0
  103. spotifywebapipython/spotifyconnect/spotifyconnectdeviceeventargs.py +67 -0
  104. spotifywebapipython/spotifyconnect/spotifyconnectdirectorytask.py +2564 -0
  105. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastapptask.py +467 -0
  106. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastcontroller.py +701 -0
  107. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastlistener.py +359 -0
  108. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastmultizonelistener.py +212 -0
  109. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfexceptions.py +20 -0
  110. spotifywebapipython/spotifyconnect/spotifyconnectzeroconflistener.py +335 -0
  111. spotifywebapipython/spotifydiscovery.py +473 -0
  112. spotifywebapipython/spotifymediatypes.py +53 -0
  113. spotifywebapipython/spotifywebapiauthenticationerror.py +122 -0
  114. spotifywebapipython/spotifywebapierror.py +106 -0
  115. spotifywebapipython/spotifywebplayertoken.py +743 -0
  116. spotifywebapipython/vibrant/__init__.py +17 -0
  117. spotifywebapipython/vibrant/generator.py +261 -0
  118. spotifywebapipython/vibrant/image.py +100 -0
  119. spotifywebapipython/vibrant/main.py +34 -0
  120. spotifywebapipython/vibrant/models.py +93 -0
  121. spotifywebapipython/vibrant/utils.py +56 -0
  122. spotifywebapipython/zeroconfapi/__init__.py +23 -0
  123. spotifywebapipython/zeroconfapi/authenticationtypes.py +38 -0
  124. spotifywebapipython/zeroconfapi/blobbuilder.py +370 -0
  125. spotifywebapipython/zeroconfapi/credentials.py +37 -0
  126. spotifywebapipython/zeroconfapi/cryptodiffiehellman.py +89 -0
  127. spotifywebapipython/zeroconfapi/helpers.py +64 -0
  128. spotifywebapipython/zeroconfapi/spotifyzeroconfapierror.py +108 -0
  129. spotifywebapipython/zeroconfapi/zeroconfconnect.py +1327 -0
  130. spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +572 -0
  131. spotifywebapipython/zeroconfapi/zeroconfgetinfoalias.py +109 -0
  132. spotifywebapipython/zeroconfapi/zeroconfgetinfodrmmediaformat.py +90 -0
  133. spotifywebapipython/zeroconfapi/zeroconfresponse.py +195 -0
  134. spotifywebapipython-1.0.242.dist-info/LICENSE +22 -0
  135. spotifywebapipython-1.0.242.dist-info/METADATA +219 -0
  136. spotifywebapipython-1.0.242.dist-info/RECORD +138 -0
  137. spotifywebapipython-1.0.242.dist-info/WHEEL +5 -0
  138. spotifywebapipython-1.0.242.dist-info/top_level.txt +1 -0
@@ -0,0 +1,370 @@
1
+ import hashlib
2
+ import hmac
3
+ from Crypto.Cipher import AES
4
+ from Crypto.Util import Counter
5
+ from Crypto.Protocol.KDF import PBKDF2
6
+ from Crypto.Hash import SHA1
7
+ from base64 import b64decode, b64encode
8
+
9
+ from .credentials import Credentials
10
+ from .cryptodiffiehellman import CryptoDiffieHellman
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__)
20
+
21
+
22
+ class BlobBuilder:
23
+ """
24
+ Builds a Spotify Connect Zeroconf Blob object.
25
+
26
+ Maximum length of the zeroconf blob (SP_MAX_ZEROCONF_BLOB_LENGTH)
27
+ is 2047 characters (not counting terminating NULL).
28
+ """
29
+
30
+ AES_KEY_SIZE:int = 16 # 16 byte / 128 bit key size for AES encryption
31
+ AES_BLOCK_SIZE:int = 16 # 16 byte / 128 bit block size for AES encryption
32
+
33
+ def __init__(self,
34
+ credentials:Credentials,
35
+ device_id:str,
36
+ remotePublicKeyBase64String:str,
37
+ originDeviceName:str=None,
38
+ ) -> None:
39
+ """
40
+ Initializes a new instance of the class.
41
+
42
+ Args:
43
+ credentials (Credentials):
44
+ deviceId (str):
45
+ remotePublicKeyBase64String (str):
46
+ Remote public key value in a base64-encoded string format.
47
+ originDeviceName (str):
48
+ """
49
+ # validation.
50
+ if (originDeviceName is None) or (not isinstance(originDeviceName,str)):
51
+ originDeviceName:str = 'ha-spotifyplus'
52
+
53
+ # initialize storage.
54
+ self._EncryptedBlob:bytes = b''
55
+ self._EncryptedBlobSigned:bytes = b''
56
+ self._EncryptedBlobSignedBase64String:str = ''
57
+ self._RemotePublicKeyBase64String:str = remotePublicKeyBase64String
58
+ self._RemotePublicKey:int = b64_to_int(remotePublicKeyBase64String)
59
+
60
+ # formulate origin device information.
61
+ self._OriginDeviceName:str = originDeviceName
62
+ self._OriginDeviceId:str = hashlib.sha1(bytes(self._OriginDeviceName, 'ascii')).digest().hex()
63
+
64
+ self.credentials:Credentials = credentials
65
+ self.device_id:bytes = bytes(device_id, 'ascii')
66
+ self.dh_keys = CryptoDiffieHellman()
67
+ self._blob:bytes = b''
68
+ self._decrypted_blob:bytes = b''
69
+
70
+
71
+ @property
72
+ def EncryptedBlob(self) -> bytes:
73
+ """
74
+ Returns the unsigned encrypted blob as a bytes object.
75
+ """
76
+ return self._EncryptedBlob
77
+
78
+
79
+ @property
80
+ def EncryptedBlobSigned(self) -> bytes:
81
+ """
82
+ Returns the signed encrypted blob as a bytes object.
83
+ """
84
+ return self._EncryptedBlobSigned
85
+
86
+
87
+ @property
88
+ def EncryptedBlobSignedBase64String(self) -> str:
89
+ """
90
+ Returns the signed encrypted blob as a base64 encoded string.
91
+ """
92
+ return self._EncryptedBlobSignedBase64String
93
+
94
+
95
+ @property
96
+ def RemotePublicKeyBase64String(self) -> str:
97
+ """
98
+ Returns the remote public key value in a base64 encoded string format.
99
+ """
100
+ return self._RemotePublicKeyBase64String
101
+
102
+
103
+ @property
104
+ def RemotePublicKey(self) -> int:
105
+ """
106
+ Returns the remote public key key value in an integer format.
107
+ """
108
+ return self._RemotePublicKey
109
+
110
+
111
+ def build(self):
112
+ """
113
+ Builds the blob, encrypts it, and returns the signed encrypted blob as a base64 encoded string.
114
+ """
115
+ self._BuildBlob()
116
+ self._Encrypt()
117
+ return self._EncryptedBlobSignedBase64String
118
+
119
+
120
+ def _BuildBlob(self): # -> bytes
121
+
122
+ apiMethodName:str = 'BlobBuilder._BuildBlob'
123
+ apiMethodParms:SIMethodParmListContext = None
124
+
125
+ try:
126
+
127
+ # trace.
128
+ apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
129
+ apiMethodParms.AppendKeyValue("device_id", self.device_id)
130
+ _logsi.LogMethodParmList(SILevel.Verbose, "Building Spotify Connect ZeroConf addUser Blob", apiMethodParms)
131
+
132
+ # create the blob data that will be sent to the device.
133
+ blob = bytearray()
134
+ write_int(0x49, blob) # 'I' (eye-catcher)
135
+ write_bytes(self.credentials.username, blob) # username data (length + value)
136
+ write_int(0x50, blob) # 'P' (eye-catcher)
137
+ write_int(self.credentials.auth_type, blob) # auth_type
138
+ write_int(0x51, blob) # 'Q' (eye-catcher)
139
+ write_bytes(self.credentials.password, blob) # password data (length + value)
140
+ _logsi.LogBinary(SILevel.Verbose, "Blob Data, Unencrypted Bytes", blob)
141
+
142
+ # padding
143
+ n_zeros = BlobBuilder.AES_BLOCK_SIZE - (len(blob) % BlobBuilder.AES_BLOCK_SIZE) - 1
144
+ blob.extend([0] * n_zeros)
145
+ blob.append(n_zeros + 1)
146
+ _logsi.LogBinary(SILevel.Verbose, "Blob Data, Unencrypted Bytes (with padding)", blob)
147
+
148
+ # not sure what the following is really trying to accomplish.
149
+ # it leaves the first 16 bytes as-is, then starts performing a Bitwise XOR on
150
+ # on each byte remaining in the buffer (starting at offset 0x11 / byte 17).
151
+ blen = len(blob)
152
+ for i in range(blen - 0x11, -1, -1):
153
+ blob[blen - i - 1] ^= blob[blen - i - 0x11]
154
+ _logsi.LogBinary(SILevel.Verbose, "Blob Data, Unencrypted Bytes after XOR of bytes 16 thru %d" % (blen), blob)
155
+
156
+ # secret is the deviceId value.
157
+ secret = hashlib.sha1(self.device_id).digest()
158
+ _logsi.LogBinary(SILevel.Verbose, "Blob Data Secret, SHA1 Digest", secret)
159
+
160
+ # create a unique, strong hash for the given secret.
161
+ keys = PBKDF2(secret, self.credentials.username, 20, count=0x100, hmac_hash_module=SHA1)
162
+ key = bytearray(hashlib.sha1(keys).digest()[:20])
163
+ key.extend(bytearray([0x00, 0x00, 0x00, 0x14]))
164
+ _logsi.LogBinary(SILevel.Verbose, "Blob Data Encryption Key", key)
165
+
166
+ # create a new AES cipher that will be used to encrypt the data.
167
+ # use the hashed secret as the encryption key.
168
+ # data will be encrypted in 16-byte blocks (or chunks).
169
+ encrypted_blob = bytearray()
170
+ cipher = AES.new(key, mode=AES.MODE_ECB)
171
+
172
+ def chunker(seq, size):
173
+ return (seq[pos:pos + size] for pos in range(0, len(seq), size))
174
+
175
+ # encrypt the data.
176
+ for chunk in chunker(blob, BlobBuilder.AES_BLOCK_SIZE):
177
+ encrypted_blob.extend(cipher.encrypt(chunk))
178
+
179
+ # trace.
180
+ _logsi.LogBinary(SILevel.Verbose, "Blob Data, Encrypted Bytes", encrypted_blob)
181
+
182
+ # convert encrypted bytes to a base64-encoded string so we can
183
+ # send it to the device via an http request.
184
+ self._blob = b64encode(encrypted_blob)
185
+ _logsi.LogBinary(SILevel.Verbose, "Blob Data, Encrypted base64-encoded bytes", self._blob)
186
+
187
+ finally:
188
+
189
+ # trace.
190
+ _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
191
+
192
+
193
+ def _GetIVectorAES128(self) -> bytes:
194
+ """
195
+ Returns a new initialization vector for AES (128-bit) cryptographic functions.
196
+
197
+ Returns:
198
+ A new initialization vector of 16 bytes (128-bits).</returns>
199
+ """
200
+ iv_data:list = [253, 81, 222, 19, 70, 203, 45, 89, 141, 68, 210, 240, 93, 20, 76, 30]
201
+ return int.from_bytes(bytes(iv_data), 'big')
202
+
203
+
204
+ def _Encrypt(self):
205
+ """
206
+ Encrypts the `_blob` argument and
207
+ """
208
+
209
+ apiMethodName:str = 'BlobBuilder.Encrypt'
210
+ apiMethodParms:SIMethodParmListContext = None
211
+
212
+ try:
213
+
214
+ # trace.
215
+ apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
216
+ apiMethodParms.AppendKeyValue("RemotePublicKeyBase64String", self.RemotePublicKeyBase64String)
217
+ apiMethodParms.AppendKeyValue("RemotePublicKey", self.RemotePublicKey)
218
+ _logsi.LogMethodParmList(SILevel.Verbose, "Encrypt Spotify Connect ZeroConf addUser Blob", apiMethodParms)
219
+
220
+ # trace.
221
+ _logsi.LogInt(SILevel.Verbose, "Remote Public Key: %s" % self.RemotePublicKey, self.RemotePublicKey)
222
+
223
+ # create shared key.
224
+ shared_key:int = self.dh_keys.SharedSecret(self.RemotePublicKey)
225
+ _logsi.LogInt(SILevel.Verbose, "Shared Key: %s" % shared_key, shared_key)
226
+
227
+ b_shared_key:bytes = int_to_bytes(shared_key)
228
+ _logsi.LogBinary(SILevel.Verbose, "Shared Key bytes", b_shared_key)
229
+ base_key:bytes = hashlib.sha1(b_shared_key).digest()[:16]
230
+ _logsi.LogBinary(SILevel.Verbose, "Base Key bytes", base_key)
231
+
232
+ # create encryption key (16 byte / 128 bit key size for AES encryption).
233
+ encryption_key:bytes = hmac.new(base_key, b'encryption', 'sha1').digest()
234
+ encryption_key = encryption_key[:BlobBuilder.AES_KEY_SIZE]
235
+ _logsi.LogBinary(SILevel.Verbose, "Encryption Key (AES128-bit) bytes", encryption_key)
236
+
237
+ # create new AES encryption cipher, and encrypt the data.
238
+ iv:list[int] = [253, 81, 222, 19, 70, 203, 45, 89, 141, 68, 210, 240, 93, 20, 76, 30]
239
+ ctr_e:dict = Counter.new(128, initial_value=self._GetIVectorAES128())
240
+ cipher = AES.new(encryption_key, AES.MODE_CTR, counter=ctr_e)
241
+ encrypted_blob:bytes = cipher.encrypt(self._blob)
242
+ _logsi.LogBinary(SILevel.Verbose, "Encrypted Blob (unsigned, bytes)", encrypted_blob)
243
+
244
+ # create checksum key and checksum value of encrypted data.
245
+ checksum_key:bytes = hmac.new(base_key, b'checksum', 'sha1').digest()
246
+ _logsi.LogBinary(SILevel.Verbose, "Checksum Key: %s" % checksum_key.hex(), checksum_key)
247
+ checksum:bytes = hmac.new(checksum_key, encrypted_blob, 'sha1').digest()
248
+ _logsi.LogBinary(SILevel.Verbose, "Checksum Value (bytes)", checksum)
249
+
250
+ # create signed encrypted data buffer that contains the encrypted data and checksum.
251
+ self._EncryptedBlobSigned:bytearray = bytearray(iv)
252
+ self._EncryptedBlobSigned.extend(encrypted_blob)
253
+ self._EncryptedBlobSigned.extend(checksum)
254
+ _logsi.LogBinary(SILevel.Verbose, "Encrypted Blob (signed with checksum, bytes)", self._EncryptedBlobSigned)
255
+
256
+ # convert encrypted bytes to a base64-encoded string.
257
+ b64result_bytes:bytes = b64encode(self._EncryptedBlobSigned)
258
+ self._EncryptedBlobSignedBase64String:str = b64result_bytes.decode('ascii')
259
+ _logsi.LogText(SILevel.Verbose, "Encrypted Blob (signed with checksum, base64-encoded string)", self._EncryptedBlobSignedBase64String)
260
+
261
+ finally:
262
+
263
+ # trace.
264
+ _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
265
+
266
+
267
+ # def _Decrypt(
268
+ # self,
269
+ # encrypted_blob_signed:str,
270
+ # ) -> None:
271
+ # """
272
+ # Decreypts the specified blob that was previously encrypted and signed by the
273
+ # `Encrypt` method.
274
+
275
+ # Args:
276
+ # encrypted_blob_signed (str):
277
+ # A base64 encoded string that represents a signed and encrypted data blob; the
278
+ # packet consists of the IV, data, and checksum blocks.
279
+ # """
280
+
281
+ # apiMethodName:str = 'BlobBuilder.Decrypt'
282
+ # apiMethodParms:SIMethodParmListContext = None
283
+
284
+ # try:
285
+
286
+ # # trace.
287
+ # apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
288
+ # apiMethodParms.AppendKeyValue("encrypted_blob_signed", encrypted_blob_signed)
289
+ # apiMethodParms.AppendKeyValue("RemotePublicKeyBase64String", self.RemotePublicKeyBase64String)
290
+ # _logsi.LogMethodParmList(SILevel.Verbose, "Decrypt Spotify Connect ZeroConf addUser Blob", apiMethodParms)
291
+
292
+ # if encrypted_blob_signed is not None:
293
+ # self._EncryptedBlobSignedBase64String = encrypted_blob_signed
294
+
295
+ # # convert base64-encoded string to bytes.
296
+ # _logsi.LogText(SILevel.Verbose, "Encrypted Blob (signed with checksum, base64-encoded string)", self._EncryptedBlobSignedBase64String)
297
+ # self._EncryptedBlobSigned:bytes = b64decode(self._EncryptedBlobSignedBase64String)
298
+ # _logsi.LogBinary(SILevel.Verbose, "Encrypted Blob (signed with checksum, bytes)", self._EncryptedBlobSigned)
299
+
300
+ # # get the individual components of the signed blob packet.
301
+ # iv = self._EncryptedBlobSigned[:16]
302
+ # self._EncryptedBlob = self._EncryptedBlobSigned[16:len(self._EncryptedBlobSigned)-20]
303
+ # checksum = self._EncryptedBlobSigned[len(self._EncryptedBlobSigned)-20:]
304
+ # _logsi.LogBinary(SILevel.Verbose, "Checksum Value (bytes)", checksum)
305
+ # _logsi.LogBinary(SILevel.Verbose, "Encrypted Blob (unsigned, bytes)", self._EncryptedBlob)
306
+
307
+ # clientKey:str = "7efznlz1HspR5tQftOUdz7cv/L6V5EuXWI596GyiQMist7piBWxfbdAjcpP9/o7WnCl5HdwgI7nY6cQ7ktz3Tsbs4kSG5mbi1QwnYi8IbJ9sdYdd8YxI3Vl6dfuG3JWP"
308
+ # result_b64decode = b64decode(clientKey)
309
+ # result_b64_to_int = b64_to_int(clientKey) # 114863 ...
310
+ # # result_string_to_int = string_to_int(clientKey) # 114863 ...
311
+ # # result_byte_list_to_int = byte_list_to_int(clientKey) # 114863 ...
312
+ # # result_dh_keys_Exchange = self.dh_keys.Exchange(clientKey) # 6243 ...
313
+ # #resultInt = self.dh_keys.Exchange(clientKey) # exception
314
+ # #resultInt = self.dh_keys.shared_secret(b64_to_int(resultBytes)) # exception
315
+ # #result_dh_keys_shared_secret = self.dh_keys.shared_secret(string_to_int(resultBytes)) # 438506 ...
316
+ # #resultInt = self.dh_keys.shared_secret(string_to_int(clientKey)) # exception ...
317
+
318
+ # remote_device_key:int = b64_to_int(clientKey)
319
+ # shared_key:int = 478549389067009439856765960469867849375843534534536997108028584308833390603433510828238916782187762759272369310194601566745030716193055009703822032238289108648174583850368649722797922715533666774901465941959787614689524297840942749
320
+ # shared_key_int_to_bytes = int_to_bytes(shared_key)
321
+ # #shared_key_b64encode = b64encode(shared_key)
322
+ # shared_key_b64encode_int_to_bytes = b64encode(int_to_bytes(shared_key))
323
+ # shared_key_b64encode_utf8 = b64encode(int_to_bytes(shared_key)).decode('utf-8')
324
+
325
+
326
+ # #shared_key:int = self.dh_keys.shared_secret(remote_device_key)
327
+ # b_shared_key:bytes = int_to_bytes(shared_key)
328
+ # base_key:bytes = hashlib.sha1(b_shared_key).digest()[:16]
329
+ # #tmpbytes = b64decode(clientKey)
330
+ # #shared_key:int = byte_list_to_int(tmpbytes)
331
+ # #shared_key:int = b64_to_int(clientKey)
332
+ # #shared_key = self.dh_keys.Exchange(clientKey)
333
+ # _logsi.LogValue(SILevel.Verbose, "Shared Key: %s" % shared_key, shared_key)
334
+ # #baseKey := func() []byte { sum := sha1.Sum(sharedSecret); return sum[:16] }()
335
+ # #b_shared_key:bytes = int_to_bytes(shared_key)
336
+ # #base_key:bytes = hashlib.sha1(b_shared_key).digest()[:16]
337
+ # #encryptionKey := func() []byte { sum := mac.Sum(nil); return sum[:16] }()
338
+ # encryption_key:bytes = hmac.new(base_key, b'encryption', 'sha1').digest()
339
+ # _logsi.LogBinary(SILevel.Verbose, "Encryption Key", encryption_key)
340
+
341
+ # # # create remote device key.
342
+ # # remote_device_key:int = b64_to_int(self.RemotePublicKeyBase64String)
343
+ # # _logsi.LogValue(SILevel.Verbose, "Remote Device Key", remote_device_key)
344
+
345
+ # # # create shared key.
346
+ # # shared_key:int = self.dh_keys.shared_secret(remote_device_key)
347
+ # # _logsi.LogValue(SILevel.Verbose, "Shared Key: %s" % shared_key, shared_key)
348
+
349
+ # # b_shared_key:bytes = int_to_bytes(shared_key)
350
+ # # base_key:bytes = hashlib.sha1(b_shared_key).digest()[:16]
351
+
352
+ # # # create encryption key.
353
+ # # encryption_key:bytes = hmac.new(base_key, b'encryption', 'sha1').digest()
354
+ # # _logsi.LogBinary(SILevel.Verbose, "Encryption Key", encryption_key)
355
+
356
+ # # create new AES cipher, and decrypt the data.
357
+ # ctr_e:dict = Counter.new(128, initial_value=byte_list_to_int(iv))
358
+ # cipher = AES.new(encryption_key[:16], AES.MODE_CTR, counter=ctr_e)
359
+ # self._decrypted_blob = cipher.decrypt(self._EncryptedBlob)
360
+ # _logsi.LogBinary(SILevel.Verbose, "Blob Data, Encrypted base64-encoded bytes", self._decrypted_blob)
361
+ # self._blob = b64decode(self._decrypted_blob)
362
+ # _logsi.LogBinary(SILevel.Verbose, "Blob Data, Encrypted Bytes", self._blob)
363
+
364
+
365
+ # # cipher.NewCTR(bc, iv).XORKeyStream(decrypted, encrypted)
366
+
367
+ # finally:
368
+
369
+ # # trace.
370
+ # _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
@@ -0,0 +1,37 @@
1
+ # our package imports.
2
+ from .authenticationtypes import AuthenticationTypes
3
+
4
+ class Credentials:
5
+ """
6
+ Credentials used by the Spotify SpConnectionLoginBlob method.
7
+ """
8
+
9
+ def __init__(
10
+ self,
11
+ username:str,
12
+ password:str,
13
+ authenticationType:AuthenticationTypes=AuthenticationTypes.USER_PASS,
14
+ ):
15
+ """
16
+ Initializes a new instance of the class.
17
+
18
+ Args:
19
+ username (str):
20
+ Spotify Connect user name to login with.
21
+ password (str):
22
+ Spotify Connect user password to login with.
23
+ authenticationType (AuthenticationTypes):
24
+ Authentication type.
25
+ """
26
+ # validations.
27
+ if (authenticationType is None) or (not isinstance(authenticationType, AuthenticationTypes)):
28
+ authenticationType = AuthenticationTypes.USER_PASS
29
+
30
+ # initialize storage.
31
+ self.username: bytes = bytes(username, 'ascii')
32
+ if (isinstance(password, bytes)):
33
+ self.password: bytes = password
34
+ else:
35
+ self.password: bytes = bytes(password, 'ascii')
36
+ self.auth_type: AuthenticationTypes = authenticationType
37
+
@@ -0,0 +1,89 @@
1
+ import secrets
2
+
3
+ from .helpers import byte_list_to_int, int_to_bytes, b64_to_int, int_to_b64str
4
+
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
+ """
12
+
13
+ DH_GENERATOR = 2
14
+ DH_PRIME_MODULUS = byte_list_to_int([
15
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2,
16
+ 0x34, 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67,
17
+ 0xcc, 0x74, 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e,
18
+ 0x34, 0x04, 0xdd, 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d,
19
+ 0xf2, 0x5f, 0x14, 0x37, 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5,
20
+ 0x76, 0x62, 0x5e, 0x7e, 0xc6, 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x3a, 0x36, 0x20, 0xff, 0xff,
21
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
22
+
23
+
24
+ def __init__(self):
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
+
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
+
58
+
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
+
@@ -0,0 +1,64 @@
1
+ from base64 import b64decode, b64encode
2
+ import binascii
3
+
4
+
5
+ def write_int(i: int, out: bytearray):
6
+ if i < 0x80:
7
+ out.append(i)
8
+ else:
9
+ out.append(0x80 | (i & 0x7f))
10
+ out.append((i >> 7))
11
+
12
+
13
+ def write_bytes(b: bytes, out: bytearray):
14
+ write_int(len(b), out)
15
+ out.extend(b)
16
+
17
+
18
+ def byte_list_to_int(byte_list) -> int:
19
+ """
20
+ Converts a list of bytes to a big integer.
21
+ """
22
+ return int.from_bytes(bytes(byte_list), 'big')
23
+
24
+
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'
30
+
31
+
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')
37
+
38
+
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 b64str_to_bytes(value: str):
47
+ """
48
+ Decodes a base64-encoded string to a byte array.
49
+ """
50
+ return b64decode(value)
51
+
52
+
53
+ def string_to_int(value):
54
+ """
55
+ Converts a string to a big integer.
56
+ """
57
+ return int(binascii.hexlify(value), 16)
58
+
59
+
60
+ def int_of_string(value):
61
+ """
62
+ Converts a string to a big integer.
63
+ """
64
+ return int(binascii.hexlify(value), 16)
@@ -0,0 +1,108 @@
1
+ # external package imports.
2
+ from smartinspectpython.sisession import SISession
3
+
4
+ # our package imports.
5
+ from ..sautils import export
6
+ from ..saappmessages import SAAppMessages
7
+
8
+
9
+ @export
10
+ class SpotifyZeroconfApiError(Exception):
11
+ """
12
+ Exception thrown when a Spotify ZeroConf API Request Error occurs.
13
+ """
14
+
15
+ def __init__(self, status:int, message:str, methodName:str, reason:str, logsi:SISession=None) -> None:
16
+ """
17
+ Initializes a new instance of the class.
18
+
19
+ Args:
20
+ status (int):
21
+ HTTP status code that is also returned in the response header.
22
+ For further information, see Response Status Codes.
23
+ message (str):
24
+ A short description of the cause of the error.
25
+ methodName (str):
26
+ Name of the client method that executed the request.
27
+ reason (str):
28
+ HTTP reason code for the error.
29
+ logsi (SISession):
30
+ Trace session object that this exception will be logged to, or null to bypass trace logging.
31
+ Default is None.
32
+ """
33
+
34
+ # initialize base class.
35
+ super().__init__(message)
36
+
37
+ # initialize class instance.
38
+ self._HttpReason:str = reason
39
+ self._Message:str = message
40
+ self._MethodName:str = methodName
41
+ self._Status:int = status
42
+
43
+ # trace.
44
+ if logsi is not None:
45
+ if (isinstance(logsi, SISession)):
46
+ logsi.LogException(str(self), self, logToSystemLogger=False)
47
+
48
+
49
+ def __repr__(self) -> str:
50
+ return self.ToString()
51
+
52
+
53
+ def __str__(self) -> str:
54
+ return self.ToString()
55
+
56
+
57
+ @property
58
+ def HttpReason(self) -> str:
59
+ """
60
+ HTTP reason code for the error.
61
+
62
+ Example: `Bad Request`.
63
+ """
64
+ return self._HttpReason
65
+
66
+
67
+ @property
68
+ def Message(self) -> str:
69
+ """
70
+ A short description of the cause of the error.
71
+
72
+ Example: `invalid id`.
73
+ """
74
+ return self._Message
75
+
76
+
77
+ @property
78
+ def MethodName(self) -> str:
79
+ """
80
+ The method being executed when the error occured.
81
+
82
+ Example: `getInfo`.
83
+ """
84
+ return self._MethodName
85
+
86
+
87
+ @property
88
+ def Status(self) -> int:
89
+ """
90
+ HTTP status code that is also returned in the response header.
91
+ For further information, see Response Status Codes.
92
+
93
+ Example: `400`
94
+ """
95
+ return self._Status
96
+
97
+
98
+ def ToString(self) -> str:
99
+ """
100
+ Returns a UI-friendly string representation of the class.
101
+ """
102
+ msg:str = SAAppMessages.MSG_SPOTIFY_ZEROCONF_API_ERROR.format(
103
+ methodname=self._MethodName,
104
+ status=self._Status,
105
+ httpreason=self._HttpReason,
106
+ message=self._Message
107
+ )
108
+ return msg