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.
- {spotifywebapiPython-1.0.72.dist-info → spotifywebapiPython-1.0.74.dist-info}/METADATA +1 -1
- {spotifywebapiPython-1.0.72.dist-info → spotifywebapiPython-1.0.74.dist-info}/RECORD +30 -29
- spotifywebapipython/const.py +1 -1
- spotifywebapipython/docs/__init__.html +20 -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/spotifyconnectdevices.py +53 -27
- spotifywebapipython/models/zeroconfdiscoveryresult.py +13 -0
- spotifywebapipython/spotifyclient.py +68 -20
- spotifywebapipython/zeroconfapi/__init__.py +2 -0
- spotifywebapipython/zeroconfapi/authenticationtypes.py +38 -0
- spotifywebapipython/zeroconfapi/blobbuilder.py +333 -66
- spotifywebapipython/zeroconfapi/credentials.py +18 -2
- spotifywebapipython/zeroconfapi/cryptodiffiehellman.py +71 -6
- spotifywebapipython/zeroconfapi/helpers.py +34 -7
- spotifywebapipython/zeroconfapi/zeroconfconnect.py +77 -40
- spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +69 -18
- {spotifywebapiPython-1.0.72.dist-info → spotifywebapiPython-1.0.74.dist-info}/LICENSE +0 -0
- {spotifywebapiPython-1.0.72.dist-info → spotifywebapiPython-1.0.74.dist-info}/WHEEL +0 -0
- {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
|
-
#
|
|
7985
|
-
#
|
|
7986
|
-
#
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
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
|
-
#
|
|
7995
|
-
info
|
|
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
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
185
|
+
# trace.
|
|
186
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
187
|
+
|
|
72
188
|
|
|
73
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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__(
|
|
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:
|
|
33
|
+
self.auth_type: AuthenticationTypes = authenticationType
|
|
34
|
+
|