spotifywebapiPython 1.0.72__py3-none-any.whl → 1.0.74__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.

Files changed (30) hide show
  1. {spotifywebapiPython-1.0.72.dist-info → spotifywebapiPython-1.0.74.dist-info}/METADATA +1 -1
  2. {spotifywebapiPython-1.0.72.dist-info → spotifywebapiPython-1.0.74.dist-info}/RECORD +30 -29
  3. spotifywebapipython/const.py +1 -1
  4. spotifywebapipython/docs/__init__.html +20 -2
  5. spotifywebapipython/docs/index.html +2 -2
  6. spotifywebapipython/docs/search.js +1 -1
  7. spotifywebapipython/docs/spotifywebapiPython/const.html +3 -3
  8. spotifywebapipython/docs/spotifywebapiPython/saappmessages.html +2 -2
  9. spotifywebapipython/docs/spotifywebapiPython/sautils.html +2 -2
  10. spotifywebapipython/docs/spotifywebapiPython/spotifyapierror.html +2 -2
  11. spotifywebapipython/docs/spotifywebapiPython/spotifyapimessage.html +2 -2
  12. spotifywebapipython/docs/spotifywebapiPython/spotifyauthtoken.html +2 -2
  13. spotifywebapipython/docs/spotifywebapiPython/spotifyclient.html +8 -2
  14. spotifywebapipython/docs/spotifywebapiPython/spotifydiscovery.html +2 -2
  15. spotifywebapipython/docs/spotifywebapiPython/spotifywebapiauthenticationerror.html +2 -2
  16. spotifywebapipython/docs/spotifywebapiPython/spotifywebapierror.html +2 -2
  17. spotifywebapipython/models/spotifyconnectdevices.py +53 -27
  18. spotifywebapipython/models/zeroconfdiscoveryresult.py +13 -0
  19. spotifywebapipython/spotifyclient.py +68 -20
  20. spotifywebapipython/zeroconfapi/__init__.py +2 -0
  21. spotifywebapipython/zeroconfapi/authenticationtypes.py +38 -0
  22. spotifywebapipython/zeroconfapi/blobbuilder.py +333 -66
  23. spotifywebapipython/zeroconfapi/credentials.py +18 -2
  24. spotifywebapipython/zeroconfapi/cryptodiffiehellman.py +71 -6
  25. spotifywebapipython/zeroconfapi/helpers.py +34 -7
  26. spotifywebapipython/zeroconfapi/zeroconfconnect.py +77 -40
  27. spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +69 -18
  28. {spotifywebapiPython-1.0.72.dist-info → spotifywebapiPython-1.0.74.dist-info}/LICENSE +0 -0
  29. {spotifywebapiPython-1.0.72.dist-info → spotifywebapiPython-1.0.74.dist-info}/WHEEL +0 -0
  30. {spotifywebapiPython-1.0.72.dist-info → spotifywebapiPython-1.0.74.dist-info}/top_level.txt +0 -0
@@ -4,6 +4,7 @@ from datetime import datetime
4
4
  import json
5
5
  from io import BytesIO
6
6
  from oauthlib.oauth2 import BackendApplicationClient, WebApplicationClient
7
+ import socket
7
8
  import time
8
9
  from typing import Tuple, Callable
9
10
  from urllib3 import PoolManager, Timeout, HTTPResponse
@@ -6058,7 +6059,6 @@ class SpotifyClient:
6058
6059
  zconn:ZeroconfConnect = ZeroconfConnect(discoverResult.HostIpAddress,
6059
6060
  discoverResult.HostIpPort,
6060
6061
  discoverResult.SpotifyConnectCPath,
6061
- discoverResult.SpotifyConnectVersion,
6062
6062
  useSSL=False)
6063
6063
 
6064
6064
  # if a different user context has control of the device then we need to disconnect
@@ -7921,6 +7921,12 @@ class SpotifyClient:
7921
7921
  available Spotify Connect devices that are known to the local network (versus
7922
7922
  just the devices known to a specific user).
7923
7923
 
7924
+ It first queries Zeroconf to retrieve a list of all known Spotify Connect devices on
7925
+ the local network. It will then query each one of the found device instances to obtain
7926
+ information about the device. It will try to reach the device by its direct HostIpAddress
7927
+ first; if that fails, then it will try to reach the device by its Server alias; if that
7928
+ fails, then it will log a warning that the device could not be reached and ignore it.
7929
+
7924
7930
  It will also call the `GetPlayerDevices` method, in case we have any dynamic devices.
7925
7931
  Dynamic devices are Spotify Connect devices that are not found in Zeroconf discovery
7926
7932
  process, but still exist in the player device list. These are usually Spotify Connect
@@ -7929,7 +7935,7 @@ class SpotifyClient:
7929
7935
  The `ConfigurationCache` is updated with the results of this method. Use the
7930
7936
  `refresh` argument (with False value) to retrieve the cached value and avoid
7931
7937
  the spotify web api request. This results in better performance.
7932
-
7938
+
7933
7939
  <details>
7934
7940
  <summary>Sample Code</summary>
7935
7941
  ```python
@@ -7978,27 +7984,63 @@ class SpotifyClient:
7978
7984
  _logsi.LogObject(SILevel.Verbose, 'Spotify Connect Zeroconf GetInformation call was already processed for Instance Name: "%s" (%s)' % (discoverResult.DeviceName, urlGetInfo), discoverResult)
7979
7985
 
7980
7986
  else:
7981
-
7987
+
7982
7988
  # get the id from the device via the zeroconf API getInfo endpoint, as the id is not
7983
7989
  # returned in the zeroconf discovery result.
7984
- # note that we are using the "HostIpAddress" property value here, with "Server" as a fallback.
7985
- # the "Server" property is an alias, which must be resolved via a DNS lookup under
7986
- # the covers and adds a significant delay (2-3 seconds!) to the activation time.
7987
- zconn:ZeroconfConnect = ZeroconfConnect(discoverResult.HostIpAddress,
7988
- discoverResult.HostIpPort,
7989
- discoverResult.SpotifyConnectCPath,
7990
- discoverResult.SpotifyConnectVersion,
7991
- useSSL=False)
7992
- info:ZeroconfGetInfo = zconn.GetInformation()
7990
+ # we will try to reach the device by its direct HostIpAddress first;
7991
+ # if that fails, then we will try to reach the device by its Server alias;
7992
+ # if that fails, then we will log a warning that the device could not be reached and press on.
7993
+ # note that the "Server" property is an alias, which must be resolved via a DNS lookup
7994
+ # adds a significant delay (2-3 seconds!) to the discovery time.
7995
+ zconn:ZeroconfConnect = None
7996
+ info:ZeroconfGetInfo = None
7997
+
7998
+ try:
7999
+
8000
+ # get device information from the direct ip address.
8001
+ # this is usually 1-3 seconds faster than using the alias, due to DNS resolution.
8002
+ zconn = ZeroconfConnect(discoverResult.HostIpAddress,
8003
+ discoverResult.HostIpPort,
8004
+ discoverResult.SpotifyConnectCPath,
8005
+ useSSL=False)
8006
+ info = zconn.GetInformation()
8007
+
8008
+ except Exception as ex:
8009
+
8010
+ # trace.
8011
+ _logsi.LogWarning('Spotify Connect Zeroconf GetInformation call failed for Instance Name "%s" (%s); retrying with Zeroconf DNS server alias "%s"' % (discoverResult.DeviceName, discoverResult.HostIpAddress, discoverResult.Server))
8012
+
8013
+ try:
8014
+
8015
+ # get device information using the server DNS alias.
8016
+ zconn = ZeroconfConnect(discoverResult.Server,
8017
+ discoverResult.HostIpPort,
8018
+ discoverResult.SpotifyConnectCPath,
8019
+ useSSL=False)
8020
+ info = zconn.GetInformation()
8021
+
8022
+ # update HostIpAddress in discovery result so it knows to use the alias
8023
+ # instead of the ip address.
8024
+ _logsi.LogVerbose('Spotify Connect Zeroconf GetInformation call for Instance Name "%s" (%s) was resolved using the Server alias; the HostIpAddress will be updated with the resolved address' % (discoverResult.DeviceName, discoverResult.Server))
8025
+ resolvedIpAddress = socket.gethostbyname(discoverResult.Server)
8026
+ discoverResult.HostIpAddress = resolvedIpAddress
8027
+
8028
+ except Exception as ex:
8029
+
8030
+ # trace, and log warning message.
8031
+ _logsi.LogWarning('Spotify Connect Zeroconf GetInformation call failed for Instance Name "%s" (%s); instance will be ignored, as it is either powered off or not reachable via the local network' % (discoverResult.DeviceName, discoverResult.Server))
7993
8032
 
7994
- # reset the `IsInDeviceList` indicator, as we will verify later in this method.
7995
- info.IsInDeviceList = False
8033
+ # did we find device information?
8034
+ if info is not None:
8035
+
8036
+ # reset the `IsInDeviceList` indicator, as we will verify later in this method.
8037
+ info.IsInDeviceList = False
7996
8038
 
7997
- # create new spotify connect device object.
7998
- scDevice:SpotifyConnectDevice = SpotifyConnectDevice()
7999
- scDevice.DiscoveryResult = discoverResult
8000
- scDevice.DeviceInfo = info
8001
- result.Items.append(scDevice)
8039
+ # create new spotify connect device object.
8040
+ scDevice:SpotifyConnectDevice = SpotifyConnectDevice()
8041
+ scDevice.DiscoveryResult = discoverResult
8042
+ scDevice.DeviceInfo = info
8043
+ result.Items.append(scDevice)
8002
8044
 
8003
8045
  # at this point we have processed all zeroconf discovery results.
8004
8046
  # we will now call the GetPlayerDevices method, in case we have any dynamic devices.
@@ -10458,6 +10500,13 @@ class SpotifyClient:
10458
10500
  _logsi.LogVerbose("Spotify Connect user context '%s' was verified for Device id '%s'; switch not necessary" % (deviceActiveUser, deviceIdResult))
10459
10501
  break
10460
10502
 
10503
+ # is this a Sonos device with no active user?
10504
+ # if so, then don't bother trying to switch the user context as the Sonos
10505
+ # device is a "restricted" type and cannot be controlled via Spotify WebServices API.
10506
+ if (info.IsBrandSonos) and (not info.HasActiveUser):
10507
+ _logsi.LogVerbose("Spotify Connect Device id '%s' is a Sonos device; user account switching is not supported" % (deviceIdResult))
10508
+ break
10509
+
10461
10510
  zcfResult:ZeroconfResponse
10462
10511
 
10463
10512
  # create a ZeroconfConnect object to access the device.
@@ -10467,7 +10516,6 @@ class SpotifyClient:
10467
10516
  zconn:ZeroconfConnect = ZeroconfConnect(discoverResult.HostIpAddress,
10468
10517
  discoverResult.HostIpPort,
10469
10518
  discoverResult.SpotifyConnectCPath,
10470
- discoverResult.SpotifyConnectVersion,
10471
10519
  useSSL=False)
10472
10520
 
10473
10521
  # if a different user context has control of the device then we need to disconnect
@@ -1,4 +1,5 @@
1
1
  # import all classes from the namespace.
2
+ from .authenticationtypes import AuthenticationTypes
2
3
  from .blobbuilder import BlobBuilder
3
4
  from .credentials import Credentials
4
5
  from .spotifyzeroconfapierror import SpotifyZeroconfApiError
@@ -10,6 +11,7 @@ from .zeroconfgetinfodrmmediaformat import ZeroconfGetInfoDrmMediaFormat
10
11
 
11
12
  # all classes to import when "import *" is specified.
12
13
  __all__ = [
14
+ 'AuthenticationTypes',
13
15
  'BlobBuilder',
14
16
  'Credentials',
15
17
  'SpotifyZeroconfApiError',
@@ -0,0 +1,38 @@
1
+ # external package imports.
2
+ from enum import IntEnum
3
+
4
+ # our package imports.
5
+ from ..sautils import export
6
+
7
+
8
+ @export
9
+ class AuthenticationTypes(IntEnum):
10
+ """
11
+ Represents the type of authentication used.
12
+ """
13
+
14
+ USER_PASS = 0
15
+ """
16
+ UserId and Password (0x00).
17
+ """
18
+
19
+ STORED_SPOTIFY_CREDENTIALS = 1
20
+ """
21
+ Spotify credentials (0x01).
22
+ """
23
+
24
+ STORED_FACEBOOK_CREDENTIALS = 2
25
+ """
26
+ Facebook credentials (0x02).
27
+ """
28
+
29
+ SPOTIFY_TOKEN = 3
30
+ """
31
+ Spotify authorization token (0x03).
32
+ """
33
+
34
+ FACEBOOK_TOKEN = 4
35
+ """
36
+ Facebook authorization token (0x04).
37
+ """
38
+
@@ -8,92 +8,359 @@ 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:
23
+ """
24
+ Builds a Spotify Connect Zeroconf Blob object.
15
25
 
16
- def __init__(self, credentials: Credentials, device_id: str, remote_pubkey: str):
17
- self.credentials: Credentials = credentials
18
- self.device_id: bytes = bytes(device_id, 'ascii')
19
- self.remote_pubkey: str = remote_pubkey
20
- self.dh_keys = CryptoDiffieHellman()
21
- self._blob = b''
22
- self._encrypted_blob = b''
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, credentials:Credentials, device_id:str, remotePublicKeyBase64String:str):
34
+ """
35
+ Initializes a new instance of the class.
36
+
37
+ Args:
38
+ credentials (Credentials):
39
+ deviceId (str):
40
+ remotePublicKeyBase64String (str):
41
+ Remote public key value in a base64-encoded string format.
42
+ """
43
+
44
+ # initialize storage.
45
+ self._EncryptedBlob:bytes = b''
46
+ self._EncryptedBlobSigned:bytes = b''
47
+ self._EncryptedBlobSignedBase64String:str = ''
48
+ self._RemotePublicKeyBase64String:str = remotePublicKeyBase64String
49
+ self._RemotePublicKey:int = b64_to_int(remotePublicKeyBase64String)
50
+
23
51
  # formulate origin device information.
24
- self._OriginDeviceName = 'ha-spotifyplus'
25
- self._OriginDeviceId = hashlib.sha1(bytes(self._OriginDeviceName, 'ascii')).digest().hex()
52
+ self._OriginDeviceName:str = 'ha-spotifyplus'
53
+ self._OriginDeviceId:str = hashlib.sha1(bytes(self._OriginDeviceName, 'ascii')).digest().hex()
54
+
55
+ self.credentials:Credentials = credentials
56
+ self.device_id:bytes = bytes(device_id, 'ascii')
57
+ self.dh_keys = CryptoDiffieHellman()
58
+ self._blob:bytes = b''
59
+ self._decrypted_blob:bytes = b''
60
+
61
+
62
+ @property
63
+ def EncryptedBlob(self) -> bytes:
64
+ """
65
+ Returns the unsigned encrypted blob as a bytes object.
66
+ """
67
+ return self._EncryptedBlob
68
+
69
+
70
+ @property
71
+ def EncryptedBlobSigned(self) -> bytes:
72
+ """
73
+ Returns the signed encrypted blob as a bytes object.
74
+ """
75
+ return self._EncryptedBlobSigned
76
+
77
+
78
+ @property
79
+ def EncryptedBlobSignedBase64String(self) -> str:
80
+ """
81
+ Returns the signed encrypted blob as a base64 encoded string.
82
+ """
83
+ return self._EncryptedBlobSignedBase64String
84
+
85
+
86
+ @property
87
+ def RemotePublicKeyBase64String(self) -> str:
88
+ """
89
+ Returns the remote public key value in a base64 encoded string format.
90
+ """
91
+ return self._RemotePublicKeyBase64String
92
+
93
+
94
+ @property
95
+ def RemotePublicKey(self) -> int:
96
+ """
97
+ Returns the remote public key key value in an integer format.
98
+ """
99
+ return self._RemotePublicKey
26
100
 
27
101
 
28
102
  def build(self):
29
- self._build()
30
- self._encrypt()
31
- return self._encrypted_blob.decode("ascii")
32
-
33
-
34
- def _build(self): # -> bytes
35
- blob = bytearray()
36
- # 'I'
37
- write_int(0x49, blob)
38
- # username
39
- write_bytes(self.credentials.username, blob)
40
- # 'P'
41
- write_int(0x50, blob)
42
- # auth_type
43
- write_int(self.credentials.auth_type, blob)
44
- # 'Q'
45
- write_int(0x51, blob)
46
- # password
47
- write_bytes(self.credentials.password, blob)
48
- # Padding
49
- n_zeros = 16 - (len(blob) % 16) - 1
50
- blob.extend([0] * n_zeros)
51
- blob.append(n_zeros + 1)
52
-
53
- blen = len(blob)
54
- for i in range(blen - 0x11, -1, -1):
55
- blob[blen - i - 1] ^= blob[blen - i - 0x11]
103
+ """
104
+ Builds the blob, encrypts it, and returns the signed encrypted blob as a base64 encoded string.
105
+ """
106
+ self._BuildBlob()
107
+ self._Encrypt()
108
+ return self._EncryptedBlobSignedBase64String
109
+
110
+
111
+ def _BuildBlob(self): # -> bytes
112
+
113
+ apiMethodName:str = 'BlobBuilder._BuildBlob'
114
+ apiMethodParms:SIMethodParmListContext = None
115
+
116
+ try:
117
+
118
+ # trace.
119
+ apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
120
+ apiMethodParms.AppendKeyValue("device_id", self.device_id)
121
+ _logsi.LogMethodParmList(SILevel.Verbose, "Building Spotify Connect ZeroConf addUser Blob", apiMethodParms)
122
+
123
+ # create the blob data that will be sent to the device.
124
+ blob = bytearray()
125
+ # 'I'
126
+ write_int(0x49, blob)
127
+ # username
128
+ write_bytes(self.credentials.username, blob)
129
+ # 'P'
130
+ write_int(0x50, blob)
131
+ # auth_type
132
+ write_int(self.credentials.auth_type, blob)
133
+ # 'Q'
134
+ write_int(0x51, blob)
135
+ # password
136
+ write_bytes(self.credentials.password, blob)
137
+ _logsi.LogBinary(SILevel.Verbose, "Blob Data, Unencrypted Bytes", blob)
138
+ # padding
139
+ n_zeros = BlobBuilder.AES_BLOCK_SIZE - (len(blob) % BlobBuilder.AES_BLOCK_SIZE) - 1
140
+ blob.extend([0] * n_zeros)
141
+ blob.append(n_zeros + 1)
142
+ _logsi.LogBinary(SILevel.Verbose, "Blob Data, Unencrypted Bytes (with padding)", blob)
143
+
144
+ # not sure what the following is really trying to accomplish.
145
+ # it leaves the first 16 bytes as-is, then starts performing a Bitwise XOR on
146
+ # on each byte remaining in the buffer (starting at offset 0x11 / byte 17).
147
+ blen = len(blob)
148
+ for i in range(blen - 0x11, -1, -1):
149
+ blob[blen - i - 1] ^= blob[blen - i - 0x11]
150
+ _logsi.LogBinary(SILevel.Verbose, "Blob Data, Unencrypted Bytes after XOR of bytes 16 thru %d" % (blen), blob)
56
151
 
57
- secret = hashlib.sha1(self.device_id).digest()
152
+ # secret is the deviceId value.
153
+ secret = hashlib.sha1(self.device_id).digest()
154
+ _logsi.LogBinary(SILevel.Verbose, "Blob Data Secret, SHA1 Digest", secret)
58
155
 
59
- keys = PBKDF2(secret, self.credentials.username, 20, count=0x100, hmac_hash_module=SHA1)
60
- key = bytearray(hashlib.sha1(keys).digest()[:20])
61
- key.extend(bytearray([0x00, 0x00, 0x00, 0x14]))
156
+ # create a unique, strong hash for the given secret.
157
+ keys = PBKDF2(secret, self.credentials.username, 20, count=0x100, hmac_hash_module=SHA1)
158
+ key = bytearray(hashlib.sha1(keys).digest()[:20])
159
+ key.extend(bytearray([0x00, 0x00, 0x00, 0x14]))
160
+ _logsi.LogBinary(SILevel.Verbose, "Blob Data Encryption Key", key)
62
161
 
63
- encrypted_blob = bytearray()
64
- cipher = AES.new(key, mode=AES.MODE_ECB)
65
- block_size = 16
162
+ # create a new AES cipher that will be used to encrypt the data.
163
+ # use the hashed secret as the encryption key.
164
+ # data will be encrypted in 16-byte blocks (or chunks).
165
+ encrypted_blob = bytearray()
166
+ cipher = AES.new(key, mode=AES.MODE_ECB)
167
+
168
+ def chunker(seq, size):
169
+ return (seq[pos:pos + size] for pos in range(0, len(seq), size))
66
170
 
67
- def chunker(seq, size):
68
- return (seq[pos:pos + size] for pos in range(0, len(seq), size))
171
+ # encrypt the data.
172
+ for chunk in chunker(blob, BlobBuilder.AES_BLOCK_SIZE):
173
+ encrypted_blob.extend(cipher.encrypt(chunk))
174
+
175
+ # trace.
176
+ _logsi.LogBinary(SILevel.Verbose, "Blob Data, Encrypted Bytes", encrypted_blob)
177
+
178
+ # convert encrypted bytes to a base64-encoded string so we can
179
+ # send it to the device via an http request.
180
+ self._blob = b64encode(encrypted_blob)
181
+ _logsi.LogBinary(SILevel.Verbose, "Blob Data, Encrypted base64-encoded bytes", self._blob)
182
+
183
+ finally:
69
184
 
70
- for chunk in chunker(blob, block_size):
71
- encrypted_blob.extend(cipher.encrypt(chunk))
185
+ # trace.
186
+ _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
187
+
72
188
 
73
- self._blob = b64encode(encrypted_blob)
189
+ def _GetIVectorAES128(self) -> bytes:
190
+ """
191
+ Returns a new initialization vector for AES (128-bit) cryptographic functions.
74
192
 
193
+ Returns:
194
+ A new initialization vector of 16 bytes (128-bits).</returns>
195
+ """
196
+ iv_data:list = [253, 81, 222, 19, 70, 203, 45, 89, 141, 68, 210, 240, 93, 20, 76, 30]
197
+ return int.from_bytes(bytes(iv_data), 'big')
75
198
 
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
199
 
80
- b_shared_key = int_to_bytes(shared_key)
81
- base_key = hashlib.sha1(b_shared_key).digest()[:16]
200
+ def _Encrypt(self):
201
+ """
202
+ Encrypts the `_blob` argument and
203
+ """
204
+
205
+ apiMethodName:str = 'BlobBuilder.Encrypt'
206
+ apiMethodParms:SIMethodParmListContext = None
207
+
208
+ try:
209
+
210
+ # trace.
211
+ apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
212
+ apiMethodParms.AppendKeyValue("RemotePublicKeyBase64String", self.RemotePublicKeyBase64String)
213
+ apiMethodParms.AppendKeyValue("RemotePublicKey", self.RemotePublicKey)
214
+ _logsi.LogMethodParmList(SILevel.Verbose, "Encrypt Spotify Connect ZeroConf addUser Blob", apiMethodParms)
215
+
216
+ # trace.
217
+ _logsi.LogInt(SILevel.Verbose, "Remote Public Key: %s" % self.RemotePublicKey, self.RemotePublicKey)
218
+
219
+ # create shared key.
220
+ shared_key:int = self.dh_keys.SharedSecret(self.RemotePublicKey)
221
+ _logsi.LogInt(SILevel.Verbose, "Shared Key: %s" % shared_key, shared_key)
222
+
223
+ b_shared_key:bytes = int_to_bytes(shared_key)
224
+ _logsi.LogBinary(SILevel.Verbose, "Shared Key bytes", b_shared_key)
225
+ base_key:bytes = hashlib.sha1(b_shared_key).digest()[:16]
226
+ _logsi.LogBinary(SILevel.Verbose, "Base Key bytes", base_key)
82
227
 
83
- checksum_key = hmac.new(base_key, b'checksum', 'sha1').digest()
84
- encryption_key = hmac.new(base_key, b'encryption', 'sha1').digest()
228
+ # create encryption key (16 byte / 128 bit key size for AES encryption).
229
+ encryption_key:bytes = hmac.new(base_key, b'encryption', 'sha1').digest()
230
+ encryption_key = encryption_key[:BlobBuilder.AES_KEY_SIZE]
231
+ _logsi.LogBinary(SILevel.Verbose, "Encryption Key (AES128-bit) bytes", encryption_key)
232
+
233
+ # create new AES encryption cipher, and encrypt the data.
234
+ iv:list[int] = [253, 81, 222, 19, 70, 203, 45, 89, 141, 68, 210, 240, 93, 20, 76, 30]
235
+ ctr_e:dict = Counter.new(128, initial_value=self._GetIVectorAES128())
236
+ cipher = AES.new(encryption_key, AES.MODE_CTR, counter=ctr_e)
237
+ encrypted_blob:bytes = cipher.encrypt(self._blob)
238
+ _logsi.LogBinary(SILevel.Verbose, "Encrypted Blob (unsigned, bytes)", encrypted_blob)
239
+
240
+ # create checksum key and checksum value of encrypted data.
241
+ checksum_key:bytes = hmac.new(base_key, b'checksum', 'sha1').digest()
242
+ _logsi.LogBinary(SILevel.Verbose, "Checksum Key: %s" % checksum_key.hex(), checksum_key)
243
+ checksum:bytes = hmac.new(checksum_key, encrypted_blob, 'sha1').digest()
244
+ _logsi.LogBinary(SILevel.Verbose, "Checksum Value (bytes)", checksum)
85
245
 
86
- iv = [253, 81, 222, 19, 70, 203, 45, 89, 141, 68, 210, 240, 93, 20, 76, 30]
87
- ctr_e = Counter.new(128, initial_value=byte_list_to_int(iv))
88
- cipher = AES.new(encryption_key[:16], AES.MODE_CTR, counter=ctr_e)
89
- encrypted_blob = cipher.encrypt(self._blob)
246
+ # create signed encrypted data buffer that contains the encrypted data and checksum.
247
+ self._EncryptedBlobSigned:bytearray = bytearray(iv)
248
+ self._EncryptedBlobSigned.extend(encrypted_blob)
249
+ self._EncryptedBlobSigned.extend(checksum)
250
+ _logsi.LogBinary(SILevel.Verbose, "Encrypted Blob (signed with checksum, bytes)", self._EncryptedBlobSigned)
90
251
 
91
- checksum = hmac.new(checksum_key, encrypted_blob, 'sha1').digest()
252
+ # convert encrypted bytes to a base64-encoded string.
253
+ b64result_bytes:bytes = b64encode(self._EncryptedBlobSigned)
254
+ self._EncryptedBlobSignedBase64String:str = b64result_bytes.decode('ascii')
255
+ _logsi.LogText(SILevel.Verbose, "Encrypted Blob (signed with checksum, base64-encoded string)", self._EncryptedBlobSignedBase64String)
256
+
257
+ finally:
258
+
259
+ # trace.
260
+ _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
261
+
92
262
 
93
- encrypted_signed_blob = bytearray(iv)
94
- encrypted_signed_blob.extend(encrypted_blob)
95
- encrypted_signed_blob.extend(checksum)
263
+ # def _Decrypt(
264
+ # self,
265
+ # encrypted_blob_signed:str,
266
+ # ) -> None:
267
+ # """
268
+ # Decreypts the specified blob that was previously encrypted and signed by the
269
+ # `Encrypt` method.
96
270
 
97
- self._encrypted_blob = b64encode(encrypted_signed_blob)
271
+ # Args:
272
+ # encrypted_blob_signed (str):
273
+ # A base64 encoded string that represents a signed and encrypted data blob; the
274
+ # packet consists of the IV, data, and checksum blocks.
275
+ # """
276
+
277
+ # apiMethodName:str = 'BlobBuilder.Decrypt'
278
+ # apiMethodParms:SIMethodParmListContext = None
279
+
280
+ # try:
281
+
282
+ # # trace.
283
+ # apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
284
+ # apiMethodParms.AppendKeyValue("encrypted_blob_signed", encrypted_blob_signed)
285
+ # apiMethodParms.AppendKeyValue("RemotePublicKeyBase64String", self.RemotePublicKeyBase64String)
286
+ # _logsi.LogMethodParmList(SILevel.Verbose, "Decrypt Spotify Connect ZeroConf addUser Blob", apiMethodParms)
287
+
288
+ # if encrypted_blob_signed is not None:
289
+ # self._EncryptedBlobSignedBase64String = encrypted_blob_signed
290
+
291
+ # # convert base64-encoded string to bytes.
292
+ # _logsi.LogText(SILevel.Verbose, "Encrypted Blob (signed with checksum, base64-encoded string)", self._EncryptedBlobSignedBase64String)
293
+ # self._EncryptedBlobSigned:bytes = b64decode(self._EncryptedBlobSignedBase64String)
294
+ # _logsi.LogBinary(SILevel.Verbose, "Encrypted Blob (signed with checksum, bytes)", self._EncryptedBlobSigned)
295
+
296
+ # # get the individual components of the signed blob packet.
297
+ # iv = self._EncryptedBlobSigned[:16]
298
+ # self._EncryptedBlob = self._EncryptedBlobSigned[16:len(self._EncryptedBlobSigned)-20]
299
+ # checksum = self._EncryptedBlobSigned[len(self._EncryptedBlobSigned)-20:]
300
+ # _logsi.LogBinary(SILevel.Verbose, "Checksum Value (bytes)", checksum)
301
+ # _logsi.LogBinary(SILevel.Verbose, "Encrypted Blob (unsigned, bytes)", self._EncryptedBlob)
302
+
303
+ # clientKey:str = "7efznlz1HspR5tQftOUdz7cv/L6V5EuXWI596GyiQMist7piBWxfbdAjcpP9/o7WnCl5HdwgI7nY6cQ7ktz3Tsbs4kSG5mbi1QwnYi8IbJ9sdYdd8YxI3Vl6dfuG3JWP"
304
+ # result_b64decode = b64decode(clientKey)
305
+ # result_b64_to_int = b64_to_int(clientKey) # 114863 ...
306
+ # # result_string_to_int = string_to_int(clientKey) # 114863 ...
307
+ # # result_byte_list_to_int = byte_list_to_int(clientKey) # 114863 ...
308
+ # # result_dh_keys_Exchange = self.dh_keys.Exchange(clientKey) # 6243 ...
309
+ # #resultInt = self.dh_keys.Exchange(clientKey) # exception
310
+ # #resultInt = self.dh_keys.shared_secret(b64_to_int(resultBytes)) # exception
311
+ # #result_dh_keys_shared_secret = self.dh_keys.shared_secret(string_to_int(resultBytes)) # 438506 ...
312
+ # #resultInt = self.dh_keys.shared_secret(string_to_int(clientKey)) # exception ...
313
+
314
+ # remote_device_key:int = b64_to_int(clientKey)
315
+ # shared_key:int = 478549389067009439856765960469867849375843534534536997108028584308833390603433510828238916782187762759272369310194601566745030716193055009703822032238289108648174583850368649722797922715533666774901465941959787614689524297840942749
316
+ # shared_key_int_to_bytes = int_to_bytes(shared_key)
317
+ # #shared_key_b64encode = b64encode(shared_key)
318
+ # shared_key_b64encode_int_to_bytes = b64encode(int_to_bytes(shared_key))
319
+ # shared_key_b64encode_utf8 = b64encode(int_to_bytes(shared_key)).decode('utf-8')
98
320
 
99
-
321
+
322
+ # #shared_key:int = self.dh_keys.shared_secret(remote_device_key)
323
+ # b_shared_key:bytes = int_to_bytes(shared_key)
324
+ # base_key:bytes = hashlib.sha1(b_shared_key).digest()[:16]
325
+ # #tmpbytes = b64decode(clientKey)
326
+ # #shared_key:int = byte_list_to_int(tmpbytes)
327
+ # #shared_key:int = b64_to_int(clientKey)
328
+ # #shared_key = self.dh_keys.Exchange(clientKey)
329
+ # _logsi.LogValue(SILevel.Verbose, "Shared Key: %s" % shared_key, shared_key)
330
+ # #baseKey := func() []byte { sum := sha1.Sum(sharedSecret); return sum[:16] }()
331
+ # #b_shared_key:bytes = int_to_bytes(shared_key)
332
+ # #base_key:bytes = hashlib.sha1(b_shared_key).digest()[:16]
333
+ # #encryptionKey := func() []byte { sum := mac.Sum(nil); return sum[:16] }()
334
+ # encryption_key:bytes = hmac.new(base_key, b'encryption', 'sha1').digest()
335
+ # _logsi.LogBinary(SILevel.Verbose, "Encryption Key", encryption_key)
336
+
337
+ # # # create remote device key.
338
+ # # remote_device_key:int = b64_to_int(self.RemotePublicKeyBase64String)
339
+ # # _logsi.LogValue(SILevel.Verbose, "Remote Device Key", remote_device_key)
340
+
341
+ # # # create shared key.
342
+ # # shared_key:int = self.dh_keys.shared_secret(remote_device_key)
343
+ # # _logsi.LogValue(SILevel.Verbose, "Shared Key: %s" % shared_key, shared_key)
344
+
345
+ # # b_shared_key:bytes = int_to_bytes(shared_key)
346
+ # # base_key:bytes = hashlib.sha1(b_shared_key).digest()[:16]
347
+
348
+ # # # create encryption key.
349
+ # # encryption_key:bytes = hmac.new(base_key, b'encryption', 'sha1').digest()
350
+ # # _logsi.LogBinary(SILevel.Verbose, "Encryption Key", encryption_key)
351
+
352
+ # # create new AES cipher, and decrypt the data.
353
+ # ctr_e:dict = Counter.new(128, initial_value=byte_list_to_int(iv))
354
+ # cipher = AES.new(encryption_key[:16], AES.MODE_CTR, counter=ctr_e)
355
+ # self._decrypted_blob = cipher.decrypt(self._EncryptedBlob)
356
+ # _logsi.LogBinary(SILevel.Verbose, "Blob Data, Encrypted base64-encoded bytes", self._decrypted_blob)
357
+ # self._blob = b64decode(self._decrypted_blob)
358
+ # _logsi.LogBinary(SILevel.Verbose, "Blob Data, Encrypted Bytes", self._blob)
359
+
360
+
361
+ # # cipher.NewCTR(bc, iv).XORKeyStream(decrypted, encrypted)
362
+
363
+ # finally:
364
+
365
+ # # trace.
366
+ # _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
@@ -1,9 +1,17 @@
1
+ # our package imports.
2
+ from .authenticationtypes import AuthenticationTypes
3
+
1
4
  class Credentials:
2
5
  """
3
6
  Credentials used by the Spotify SpConnectionLoginBlob method.
4
7
  """
5
8
 
6
- def __init__(self, username: str, password: str):
9
+ def __init__(
10
+ self,
11
+ username:str,
12
+ password:str,
13
+ authenticationType:AuthenticationTypes=AuthenticationTypes.USER_PASS,
14
+ ):
7
15
  """
8
16
  Initializes a new instance of the class.
9
17
 
@@ -12,7 +20,15 @@ class Credentials:
12
20
  Spotify Connect user name to login with.
13
21
  password (str):
14
22
  Spotify Connect user password to login with.
23
+ authenticationType (AuthenticationTypes):
24
+ Authentication type.
15
25
  """
26
+ # validations.
27
+ if (authenticationType is None) or (not isinstance(authenticationType, AuthenticationTypes)):
28
+ authenticationType = AuthenticationTypes.USER_PASS
29
+
30
+ # initialize storage.
16
31
  self.username: bytes = bytes(username, 'ascii')
17
32
  self.password: bytes = bytes(password, 'ascii')
18
- self.auth_type: int = 0x00
33
+ self.auth_type: AuthenticationTypes = authenticationType
34
+