@@ -2,9 +2,39 @@ import AppKit
22import WebKit
33import KeychainAccess
44import Schedule
5+ import SwiftOTP
56
67private typealias Task = _Concurrency . Task
78
9+ private enum TOTPGenerator {
10+ static func generate( serverTimeSeconds: Int ) -> String ? {
11+ let secretCipher = [ 12 , 56 , 76 , 33 , 88 , 44 , 88 , 33 , 78 , 78 , 11 , 66 , 22 , 22 , 55 , 69 , 54 ]
12+
13+ var processed = [ UInt8] ( )
14+ for (i, byte) in secretCipher. enumerated ( ) {
15+ processed. append ( UInt8 ( byte ^ ( i % 33 + 9 ) ) )
16+ }
17+
18+ let processedStr = processed. map { String ( $0) } . joined ( )
19+
20+ guard let utf8Bytes = processedStr. data ( using: . utf8) else {
21+ return nil
22+ }
23+
24+ let secretBase32 = utf8Bytes. base32EncodedString
25+
26+ guard let secretData = base32DecodeToData ( secretBase32) else {
27+ return nil
28+ }
29+
30+ guard let totp = TOTP ( secret: secretData, digits: 6 , timeInterval: 30 , algorithm: . sha1) else {
31+ return nil
32+ }
33+
34+ return totp. generate ( secondsPast1970: serverTimeSeconds)
35+ }
36+ }
37+
838struct SpotifyAccessToken : Codable {
939 let accessToken : String
1040 let accessTokenExpirationTimestampMs : TimeInterval
@@ -15,7 +45,23 @@ struct SpotifyAccessToken: Codable {
1545 }
1646
1747 static func accessToken( forCookie cookie: String ) async throws -> Self {
18- let url = URL ( string: " https://open.spotify.com/get_access_token?reason=transport&productType=web_player " ) !
48+ struct ServerTime : Codable {
49+ var serverTime : Int
50+ }
51+
52+ enum Error : Swift . Error {
53+ case totpGenerationFailed
54+ }
55+
56+ let serverTimeRequest = URLRequest ( url: . init( string: " https://open.spotify.com/server-time " ) !)
57+ let serverTimeData = try await URLSession . shared. data ( for: serverTimeRequest) . 0
58+ let serverTime = try JSONDecoder ( ) . decode ( ServerTime . self, from: serverTimeData) . serverTime
59+
60+ guard let totp = TOTPGenerator . generate ( serverTimeSeconds: serverTime) else {
61+ throw Error . totpGenerationFailed
62+ }
63+
64+ let url = URL ( string: " https://open.spotify.com/get_access_token?reason=transport&productType=web_player&totpVer=5&ts= \( Int ( Date ( ) . timeIntervalSince1970) ) &totp= \( totp) " ) !
1965 var request = URLRequest ( url: url)
2066 request. setValue ( " sp_dc= \( cookie) " , forHTTPHeaderField: " Cookie " )
2167 let accessTokenData = try await URLSession . shared. data ( for: request) . 0
@@ -99,7 +145,7 @@ public final class SpotifyLoginManager: NSObject, @unchecked Sendable {
99145 }
100146
101147 private let secureStorage = SecureStorage ( )
102-
148+
103149 private var refreshTask : Schedule . Task ?
104150
105151 public var isLogin : Bool {
@@ -134,7 +180,6 @@ public final class SpotifyLoginManager: NSObject, @unchecked Sendable {
134180 }
135181 }
136182
137-
138183 private func scheduleAccessTokenRefresh( _ accessToken: SpotifyAccessToken ) {
139184 refreshTask? . cancel ( )
140185 refreshTask = Plan . at ( accessToken. expirationDate) . do ( queue: . global( ) ) { [ weak self] in
0 commit comments