Skip to content

Commit 31641ce

Browse files
committed
Added Login5 code (#322)
1 parent 8419f7b commit 31641ce

3 files changed

Lines changed: 160 additions & 4 deletions

File tree

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* Copyright 2021 devgianlu
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package xyz.gianlu.librespot.core;
18+
19+
import com.google.protobuf.ByteString;
20+
import com.google.protobuf.Duration;
21+
import com.spotify.login5v3.ClientInfoOuterClass;
22+
import com.spotify.login5v3.Hashcash;
23+
import com.spotify.login5v3.Login5;
24+
import okhttp3.Request;
25+
import okhttp3.Response;
26+
import okhttp3.ResponseBody;
27+
import org.jetbrains.annotations.NotNull;
28+
import xyz.gianlu.librespot.mercury.MercuryRequests;
29+
30+
import java.io.IOException;
31+
import java.security.MessageDigest;
32+
import java.security.NoSuchAlgorithmException;
33+
34+
import static xyz.gianlu.librespot.dealer.ApiClient.protoBody;
35+
36+
/**
37+
* @author devgianlu
38+
*/
39+
public final class Login5Api {
40+
private final Session session;
41+
42+
public Login5Api(@NotNull Session session) {
43+
this.session = session;
44+
}
45+
46+
private static boolean checkTenTrailingBits(byte[] array) {
47+
if (array[array.length - 1] != 0) return false;
48+
else return Integer.numberOfTrailingZeros(array[array.length - 2]) >= 2;
49+
}
50+
51+
private static void incrementCtr(byte[] ctr, int index) {
52+
ctr[index]++;
53+
if (ctr[index] == 0 && index != 0)
54+
incrementCtr(ctr, index - 1);
55+
}
56+
57+
@NotNull
58+
private static ChallengeSolve solveHashCash(byte[] prefix, int length, byte[] random) throws NoSuchAlgorithmException {
59+
MessageDigest md = MessageDigest.getInstance("SHA1");
60+
61+
byte[] suffix = new byte[16];
62+
System.arraycopy(random, 0, suffix, 0, 8);
63+
64+
assert length == 10;
65+
66+
int iters = 0;
67+
while (true) {
68+
md.reset();
69+
md.update(prefix);
70+
md.update(suffix);
71+
byte[] digest = md.digest();
72+
if (checkTenTrailingBits(digest))
73+
return new ChallengeSolve(suffix, iters);
74+
75+
incrementCtr(suffix, suffix.length - 1);
76+
incrementCtr(suffix, 7);
77+
iters++;
78+
}
79+
}
80+
81+
@NotNull
82+
private static Login5.LoginRequest.Builder solveChallenge(@NotNull Login5.LoginResponse resp) throws NoSuchAlgorithmException {
83+
byte[] loginContext = resp.getLoginContext().toByteArray();
84+
85+
Hashcash.HashcashChallenge hashcash = resp.getChallenges().getChallenges(0).getHashcash();
86+
87+
byte[] prefix = hashcash.getPrefix().toByteArray();
88+
byte[] seed = new byte[8];
89+
byte[] loginContextDigest = MessageDigest.getInstance("SHA1").digest(loginContext);
90+
System.arraycopy(loginContextDigest, 12, seed, 0, 8);
91+
92+
long start = System.nanoTime();
93+
ChallengeSolve solved = solveHashCash(prefix, hashcash.getLength(), seed);
94+
long durationNano = System.nanoTime() - start;
95+
96+
return Login5.LoginRequest.newBuilder()
97+
.setLoginContext(ByteString.copyFrom(loginContext))
98+
.setChallengeSolutions(Login5.ChallengeSolutions.newBuilder()
99+
.addSolutions(Login5.ChallengeSolution.newBuilder()
100+
.setHashcash(Hashcash.HashcashSolution.newBuilder()
101+
.setDuration(Duration.newBuilder()
102+
.setSeconds((int) (durationNano / 1_000_000_000))
103+
.setNanos((int) (durationNano % 1_000_000_000))
104+
.build())
105+
.setSuffix(ByteString.copyFrom(solved.suffix))
106+
.build()).build()).build());
107+
}
108+
109+
@NotNull
110+
private Login5.LoginResponse send(@NotNull Login5.LoginRequest msg) throws IOException {
111+
Request.Builder req = new Request.Builder()
112+
.method("POST", protoBody(msg))
113+
.url("https://login5.spotify.com/v3/login");
114+
115+
try (Response resp = session.client().newCall(req.build()).execute()) {
116+
ResponseBody body = resp.body();
117+
if (body == null) throw new IOException("No body");
118+
return Login5.LoginResponse.parseFrom(body.bytes());
119+
}
120+
}
121+
122+
@NotNull
123+
public Login5.LoginResponse login5(@NotNull Login5.LoginRequest req) throws IOException, NoSuchAlgorithmException {
124+
req = req.toBuilder()
125+
.setClientInfo(ClientInfoOuterClass.ClientInfo.newBuilder()
126+
.setClientId(MercuryRequests.KEYMASTER_CLIENT_ID)
127+
.setDeviceId(session.deviceId())
128+
.build())
129+
.build();
130+
131+
Login5.LoginResponse resp = send(req);
132+
if (resp.hasChallenges()) {
133+
Login5.LoginRequest.Builder reqq = solveChallenge(resp);
134+
reqq.setClientInfo(req.getClientInfo())
135+
.setAppleSignInCredential(req.getAppleSignInCredential())
136+
.setFacebookAccessToken(req.getFacebookAccessToken())
137+
.setOneTimeToken(req.getOneTimeToken())
138+
.setPhoneNumber(req.getPhoneNumber())
139+
.setStoredCredential(req.getStoredCredential())
140+
.setPassword(req.getPassword());
141+
resp = send(reqq.build());
142+
}
143+
144+
return resp;
145+
}
146+
147+
private static class ChallengeSolve {
148+
final byte[] suffix;
149+
final int ctr;
150+
151+
ChallengeSolve(byte[] suffix, int ctr) {
152+
this.suffix = suffix;
153+
this.ctr = ctr;
154+
}
155+
}
156+
}

lib/src/main/java/xyz/gianlu/librespot/dealer/ApiClient.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@
3737
import static com.spotify.canvaz.CanvazOuterClass.EntityCanvazResponse;
3838

3939
/**
40-
* @author Gianlu
40+
* @author devgianlu
4141
*/
42-
public class ApiClient {
42+
public final class ApiClient {
4343
private static final Logger LOGGER = LoggerFactory.getLogger(ApiClient.class);
4444
private final Session session;
4545
private final String baseUrl;
@@ -50,7 +50,7 @@ public ApiClient(@NotNull Session session) {
5050
}
5151

5252
@NotNull
53-
private static RequestBody protoBody(@NotNull Message msg) {
53+
public static RequestBody protoBody(@NotNull Message msg) {
5454
return new RequestBody() {
5555
@Override
5656
public MediaType contentType() {

lib/src/main/java/xyz/gianlu/librespot/mercury/MercuryRequests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
* @author Gianlu
3636
*/
3737
public final class MercuryRequests {
38-
private static final String KEYMASTER_CLIENT_ID = "65b708073fc0480ea92a077233ca87bd";
38+
public static final String KEYMASTER_CLIENT_ID = "65b708073fc0480ea92a077233ca87bd";
3939

4040
private MercuryRequests() {
4141
}

0 commit comments

Comments
 (0)