From 59f298a935c87077a6abd50656f8a4ead44bd979 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 15 Mar 2025 01:41:16 +0600 Subject: [PATCH] fix: spotify login broken due to new totp requirement #2494 --- .../authentication/authentication.dart | 94 ++++++++++++++++++- pubspec.lock | 18 +++- pubspec.yaml | 2 +- 3 files changed, 109 insertions(+), 5 deletions(-) diff --git a/lib/provider/authentication/authentication.dart b/lib/provider/authentication/authentication.dart index 40949e68..f6c6acfb 100644 --- a/lib/provider/authentication/authentication.dart +++ b/lib/provider/authentication/authentication.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:collection/collection.dart'; @@ -15,6 +16,9 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/database/database.dart'; import 'package:spotube/utils/platform.dart'; +import 'package:otp_util/otp_util.dart'; +// ignore: implementation_imports +import 'package:otp_util/src/utils/generic_util.dart'; extension ExpirationAuthenticationTableData on AuthenticationTableData { bool get isExpired => DateTime.now().isAfter(expiration); @@ -100,6 +104,83 @@ class AuthenticationNotifier extends AsyncNotifier { .insert(refreshedCredentials, mode: InsertMode.replace); } + String base32FromBytes(Uint8List e, String secretSauce) { + var t = 0; + var n = 0; + var r = ""; + for (int i = 0; i < e.length; i++) { + n = n << 8 | e[i]; + t += 8; + while (t >= 5) { + r += secretSauce[n >>> t - 5 & 31]; + t -= 5; + } + } + if (t > 0) { + r += secretSauce[n << 5 - t & 31]; + } + return r; + } + + Uint8List cleanBuffer(String e) { + e = e.replaceAll(" ", ""); + final t = List.filled(e.length ~/ 2, 0); + final n = Uint8List.fromList(t); + for (int r = 0; r < e.length; r += 2) { + n[r ~/ 2] = int.parse(e.substring(r, r + 2), radix: 16); + } + return n; + } + + Future generateTotp() async { + const secretSauce = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + final secretCipherBytes = const [ + 12, + 56, + 76, + 33, + 88, + 44, + 88, + 33, + 78, + 78, + 11, + 66, + 22, + 22, + 55, + 69, + 54 + ].mapIndexed((t, e) => e ^ t % 33 + 9).toList(); + + final secretBytes = cleanBuffer( + utf8 + .encode(secretCipherBytes.join("")) + .map((e) => e.toRadixString(16)) + .join(), + ); + + final secret = base32FromBytes(secretBytes, secretSauce); + + final res = await dio.get("https://open.spotify.com/server-time"); + final serverTimeSeconds = res.data["serverTime"] as int; + + final totp = TOTP( + secret: secret, + algorithm: OTPAlgorithm.SHA1, + digits: 6, + interval: 30, + ); + + return totp.generateOTP( + input: Util.timeFormat( + time: DateTime.fromMillisecondsSinceEpoch(serverTimeSeconds * 1000), + interval: 30, + ), + ); + } + Future credentialsFromCookie( String cookie, ) async { @@ -108,10 +189,17 @@ class AuthenticationNotifier extends AsyncNotifier { .split("; ") .firstWhereOrNull((c) => c.trim().startsWith("sp_dc=")) ?.trim(); + + final totp = await generateTotp(); + final timestamp = (DateTime.now().millisecondsSinceEpoch / 1000).floor(); + + final accessTokenUrl = Uri.parse( + "https://open.spotify.com/get_access_token?reason=transport&productType=web_player" + "&totp=$totp&totpVer=5&ts=$timestamp", + ); + final res = await dio.getUri( - Uri.parse( - "https://open.spotify.com/get_access_token?reason=transport&productType=web_player", - ), + accessTokenUrl, options: Options( headers: { "Cookie": spDc ?? "", diff --git a/pubspec.lock b/pubspec.lock index 6bdc876f..89e33185 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -166,6 +166,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + base32: + dependency: transitive + description: + name: base32 + sha256: ddad4ebfedf93d4500818ed8e61443b734ffe7cf8a45c668c9b34ef6adde02e2 + url: "https://pub.dev" + source: hosted + version: "2.1.3" bonsoir: dependency: "direct main" description: @@ -440,7 +448,7 @@ packages: source: hosted version: "0.3.4+2" crypto: - dependency: "direct dev" + dependency: transitive description: name: crypto sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" @@ -1639,6 +1647,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.3" + otp_util: + dependency: "direct main" + description: + name: otp_util + sha256: dd8956c6472bacc3ffabe62c03f8a9782d1e5a5a3f2674420970f549d642b1cf + url: "https://pub.dev" + source: hosted + version: "1.0.2" package_config: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 98161c4b..bb7b58b5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -140,10 +140,10 @@ dependencies: url: https://github.com/KRTirtho/flutter_new_pipe_extractor.git http_parser: ^4.1.2 collection: any + otp_util: ^1.0.2 dev_dependencies: build_runner: ^2.4.13 - crypto: ^3.0.3 envied_generator: ^1.0.0 flutter_gen_runner: ^5.4.0 flutter_launcher_icons: ^0.14.2