Skip to content

Commit 59f298a

Browse files
committed
fix: spotify login broken due to new totp requirement #2494
1 parent bbe3394 commit 59f298a

3 files changed

Lines changed: 109 additions & 5 deletions

File tree

lib/provider/authentication/authentication.dart

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:async';
2+
import 'dart:convert';
23
import 'dart:io';
34

45
import 'package:collection/collection.dart';
@@ -15,6 +16,9 @@ import 'package:spotube/extensions/context.dart';
1516
import 'package:spotube/models/database/database.dart';
1617
import 'package:spotube/provider/database/database.dart';
1718
import 'package:spotube/utils/platform.dart';
19+
import 'package:otp_util/otp_util.dart';
20+
// ignore: implementation_imports
21+
import 'package:otp_util/src/utils/generic_util.dart';
1822

1923
extension ExpirationAuthenticationTableData on AuthenticationTableData {
2024
bool get isExpired => DateTime.now().isAfter(expiration);
@@ -100,6 +104,83 @@ class AuthenticationNotifier extends AsyncNotifier<AuthenticationTableData?> {
100104
.insert(refreshedCredentials, mode: InsertMode.replace);
101105
}
102106

107+
String base32FromBytes(Uint8List e, String secretSauce) {
108+
var t = 0;
109+
var n = 0;
110+
var r = "";
111+
for (int i = 0; i < e.length; i++) {
112+
n = n << 8 | e[i];
113+
t += 8;
114+
while (t >= 5) {
115+
r += secretSauce[n >>> t - 5 & 31];
116+
t -= 5;
117+
}
118+
}
119+
if (t > 0) {
120+
r += secretSauce[n << 5 - t & 31];
121+
}
122+
return r;
123+
}
124+
125+
Uint8List cleanBuffer(String e) {
126+
e = e.replaceAll(" ", "");
127+
final t = List.filled(e.length ~/ 2, 0);
128+
final n = Uint8List.fromList(t);
129+
for (int r = 0; r < e.length; r += 2) {
130+
n[r ~/ 2] = int.parse(e.substring(r, r + 2), radix: 16);
131+
}
132+
return n;
133+
}
134+
135+
Future<String> generateTotp() async {
136+
const secretSauce = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
137+
final secretCipherBytes = const [
138+
12,
139+
56,
140+
76,
141+
33,
142+
88,
143+
44,
144+
88,
145+
33,
146+
78,
147+
78,
148+
11,
149+
66,
150+
22,
151+
22,
152+
55,
153+
69,
154+
54
155+
].mapIndexed((t, e) => e ^ t % 33 + 9).toList();
156+
157+
final secretBytes = cleanBuffer(
158+
utf8
159+
.encode(secretCipherBytes.join(""))
160+
.map((e) => e.toRadixString(16))
161+
.join(),
162+
);
163+
164+
final secret = base32FromBytes(secretBytes, secretSauce);
165+
166+
final res = await dio.get("https://open.spotify.com/server-time");
167+
final serverTimeSeconds = res.data["serverTime"] as int;
168+
169+
final totp = TOTP(
170+
secret: secret,
171+
algorithm: OTPAlgorithm.SHA1,
172+
digits: 6,
173+
interval: 30,
174+
);
175+
176+
return totp.generateOTP(
177+
input: Util.timeFormat(
178+
time: DateTime.fromMillisecondsSinceEpoch(serverTimeSeconds * 1000),
179+
interval: 30,
180+
),
181+
);
182+
}
183+
103184
Future<AuthenticationTableCompanion> credentialsFromCookie(
104185
String cookie,
105186
) async {
@@ -108,10 +189,17 @@ class AuthenticationNotifier extends AsyncNotifier<AuthenticationTableData?> {
108189
.split("; ")
109190
.firstWhereOrNull((c) => c.trim().startsWith("sp_dc="))
110191
?.trim();
192+
193+
final totp = await generateTotp();
194+
final timestamp = (DateTime.now().millisecondsSinceEpoch / 1000).floor();
195+
196+
final accessTokenUrl = Uri.parse(
197+
"https://open.spotify.com/get_access_token?reason=transport&productType=web_player"
198+
"&totp=$totp&totpVer=5&ts=$timestamp",
199+
);
200+
111201
final res = await dio.getUri(
112-
Uri.parse(
113-
"https://open.spotify.com/get_access_token?reason=transport&productType=web_player",
114-
),
202+
accessTokenUrl,
115203
options: Options(
116204
headers: {
117205
"Cookie": spDc ?? "",

pubspec.lock

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,14 @@ packages:
166166
url: "https://pub.dev"
167167
source: hosted
168168
version: "3.0.0"
169+
base32:
170+
dependency: transitive
171+
description:
172+
name: base32
173+
sha256: ddad4ebfedf93d4500818ed8e61443b734ffe7cf8a45c668c9b34ef6adde02e2
174+
url: "https://pub.dev"
175+
source: hosted
176+
version: "2.1.3"
169177
bonsoir:
170178
dependency: "direct main"
171179
description:
@@ -440,7 +448,7 @@ packages:
440448
source: hosted
441449
version: "0.3.4+2"
442450
crypto:
443-
dependency: "direct dev"
451+
dependency: transitive
444452
description:
445453
name: crypto
446454
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
@@ -1639,6 +1647,14 @@ packages:
16391647
url: "https://pub.dev"
16401648
source: hosted
16411649
version: "0.0.3"
1650+
otp_util:
1651+
dependency: "direct main"
1652+
description:
1653+
name: otp_util
1654+
sha256: dd8956c6472bacc3ffabe62c03f8a9782d1e5a5a3f2674420970f549d642b1cf
1655+
url: "https://pub.dev"
1656+
source: hosted
1657+
version: "1.0.2"
16421658
package_config:
16431659
dependency: transitive
16441660
description:

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,10 @@ dependencies:
140140
url: https://github.com/KRTirtho/flutter_new_pipe_extractor.git
141141
http_parser: ^4.1.2
142142
collection: any
143+
otp_util: ^1.0.2
143144

144145
dev_dependencies:
145146
build_runner: ^2.4.13
146-
crypto: ^3.0.3
147147
envied_generator: ^1.0.0
148148
flutter_gen_runner: ^5.4.0
149149
flutter_launcher_icons: ^0.14.2

0 commit comments

Comments
 (0)