diff --git a/dacp/pom.xml b/dacp/pom.xml
index 50872dee..fbb4b53f 100644
--- a/dacp/pom.xml
+++ b/dacp/pom.xml
@@ -31,10 +31,15 @@
librespot-java DACP interface
+
+ xyz.gianlu.librespot
+ librespot-lib
+ ${project.version}
+
org.slf4j
slf4j-api
${slf4j-api.version}
-
\ No newline at end of file
+
diff --git a/dacp/src/main/java/xyz/gianlu/librespot/dacp/DacpMetadataPipe.java b/dacp/src/main/java/xyz/gianlu/librespot/dacp/DacpMetadataPipe.java
index d4e74cb0..4e1a21a3 100644
--- a/dacp/src/main/java/xyz/gianlu/librespot/dacp/DacpMetadataPipe.java
+++ b/dacp/src/main/java/xyz/gianlu/librespot/dacp/DacpMetadataPipe.java
@@ -21,13 +21,13 @@
import org.jetbrains.annotations.Range;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import xyz.gianlu.librespot.common.Utils;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
-import java.util.Base64;
/**
* Metadata pipe implementation following the Shairport Sync format (https://github.com/mikebrady/shairport-sync-metadata-reader).
@@ -73,7 +73,7 @@ private synchronized void send(@NotNull String type, @NotNull String code, @Null
if (payload != null && payload.length > 0) {
out.write(String.format("- %s
%s%d\n%s \n", type, code,
- payload.length, new String(Base64.getEncoder().encode(payload), StandardCharsets.UTF_8)).getBytes(StandardCharsets.UTF_8));
+ payload.length, Utils.toBase64(payload)).getBytes(StandardCharsets.UTF_8));
} else {
out.write(String.format("- %s
%s0 \n", type, code).getBytes(StandardCharsets.UTF_8));
}
diff --git a/lib/src/main/java/xyz/gianlu/librespot/ZeroconfServer.java b/lib/src/main/java/xyz/gianlu/librespot/ZeroconfServer.java
index b11b7baa..4f3ad786 100644
--- a/lib/src/main/java/xyz/gianlu/librespot/ZeroconfServer.java
+++ b/lib/src/main/java/xyz/gianlu/librespot/ZeroconfServer.java
@@ -164,7 +164,7 @@ private ZeroconfServer(@NotNull Inner inner, int listenPort, boolean listenAllIn
public static String getUsefulHostname() throws UnknownHostException {
String host = InetAddress.getLocalHost().getHostName();
if (Objects.equals(host, "localhost")) {
- host = Base64.getEncoder().encodeToString(BigInteger.valueOf(ThreadLocalRandom.current().nextLong()).toByteArray()) + ".local";
+ host = Utils.toBase64(BigInteger.valueOf(ThreadLocalRandom.current().nextLong()).toByteArray()) + ".local";
LOGGER.warn("Hostname cannot be `localhost`, temporary hostname: " + host);
return host;
}
@@ -239,7 +239,7 @@ private void handleGetInfo(OutputStream out, String httpVersion) throws IOExcept
JsonObject info = DEFAULT_GET_INFO_FIELDS.deepCopy();
info.addProperty("deviceID", inner.deviceId);
info.addProperty("remoteName", inner.deviceName);
- info.addProperty("publicKey", Base64.getEncoder().encodeToString(keys.publicKeyArray()));
+ info.addProperty("publicKey", Utils.toBase64(keys.publicKeyArray()));
info.addProperty("deviceType", inner.deviceType.name().toUpperCase());
synchronized (connectionLock) {
@@ -292,8 +292,8 @@ private void handleAddUser(OutputStream out, Map params, String
}
}
- byte[] sharedKey = Utils.toByteArray(keys.computeSharedKey(Base64.getDecoder().decode(clientKeyStr)));
- byte[] blobBytes = Base64.getDecoder().decode(blobStr);
+ byte[] sharedKey = Utils.toByteArray(keys.computeSharedKey(Utils.fromBase64(clientKeyStr)));
+ byte[] blobBytes = Utils.fromBase64(blobStr);
byte[] iv = Arrays.copyOfRange(blobBytes, 0, 16);
byte[] encrypted = Arrays.copyOfRange(blobBytes, 16, blobBytes.length - 20);
byte[] checksum = Arrays.copyOfRange(blobBytes, blobBytes.length - 20, blobBytes.length);
diff --git a/lib/src/main/java/xyz/gianlu/librespot/common/BytesArrayList.java b/lib/src/main/java/xyz/gianlu/librespot/common/BytesArrayList.java
index 4692c14e..066f49a8 100644
--- a/lib/src/main/java/xyz/gianlu/librespot/common/BytesArrayList.java
+++ b/lib/src/main/java/xyz/gianlu/librespot/common/BytesArrayList.java
@@ -20,7 +20,6 @@
import java.io.InputStream;
import java.util.Arrays;
-import java.util.Base64;
import java.util.Iterator;
import java.util.NoSuchElementException;
@@ -44,7 +43,7 @@ private BytesArrayList(byte[][] buffer) {
@NotNull
public static InputStream streamBase64(@NotNull String[] payloads) {
byte[][] decoded = new byte[payloads.length][];
- for (int i = 0; i < decoded.length; i++) decoded[i] = Base64.getDecoder().decode(payloads[i]);
+ for (int i = 0; i < decoded.length; i++) decoded[i] = Utils.fromBase64(payloads[i]);
return new BytesArrayList(decoded).stream();
}
diff --git a/lib/src/main/java/xyz/gianlu/librespot/common/Utils.java b/lib/src/main/java/xyz/gianlu/librespot/common/Utils.java
index 391ca2cc..ffc962c8 100644
--- a/lib/src/main/java/xyz/gianlu/librespot/common/Utils.java
+++ b/lib/src/main/java/xyz/gianlu/librespot/common/Utils.java
@@ -31,9 +31,12 @@
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigInteger;
import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
import java.security.Permission;
import java.security.PermissionCollection;
import java.util.*;
@@ -45,6 +48,8 @@ public final class Utils {
private final static char[] hexArray = "0123456789ABCDEF".toCharArray();
private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class);
private static final String randomString = "abcdefghijklmnopqrstuvwxyz0123456789";
+ private static final String JAVA_UTIL_BASE_64 = "java.util.Base64";
+ private static final String ANDROID_UTIL_BASE_64 = "android.util.Base64";
private Utils() {
}
@@ -310,12 +315,70 @@ public static String artistsToString(List artists) {
}
@NotNull
- public static String toBase64(@NotNull ByteString bytes) {
- return Base64.getEncoder().encodeToString(bytes.toByteArray());
+ public static String toBase64(@NotNull byte[] bytes, boolean padding) {
+ byte[] encodedBytes;
+ try {
+ Class> clazz = Class.forName(JAVA_UTIL_BASE_64);
+ final Method getEncoder = clazz.getDeclaredMethod("getEncoder");
+ Class> encoderClazz = Class.forName("java.util.Base64$Encoder");
+ Object encoder = getEncoder.invoke(null);
+ final Method withoutPadding = encoderClazz.getDeclaredMethod("withoutPadding");
+ if (!padding)
+ encoder = withoutPadding.invoke(encoder);
+ final Method encode = encoderClazz.getDeclaredMethod("encode", byte[].class);
+ encodedBytes = (byte[]) encode.invoke(encoder, bytes);
+ } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException ignored) {
+ try {
+ Class> clazz = Class.forName(ANDROID_UTIL_BASE_64);
+ final Method encode = clazz.getDeclaredMethod("encode", byte[].class, int.class);
+ int flags = 2; // Base64.NO_WRAP
+ if (!padding)
+ flags |= 1; // Base64.NO_PADDING
+ encodedBytes = (byte[]) encode.invoke(null, bytes, flags); // Base64.NO_WRAP | Base64.NO_PADDING
+ } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException ignored2) {
+ throw new NoClassDefFoundError("Base64 not available");
+ }
+ }
+
+ return new String(encodedBytes, StandardCharsets.UTF_8);
}
@NotNull
- public static ByteString fromBase64(@NotNull String str) {
- return ByteString.copyFrom(Base64.getDecoder().decode(str.getBytes()));
+ public static String toBase64NoPadding(@NotNull byte[] bytes) {
+ return toBase64(bytes, false);
+ }
+
+ @NotNull
+ public static String toBase64(@NotNull byte[] bytes) {
+ return toBase64(bytes, true);
+ }
+
+ @NotNull
+ public static byte[] fromBase64(@NotNull String str) {
+ return fromBase64(str.getBytes());
+ }
+
+ @NotNull
+ public static byte[] fromBase64(@NotNull byte[] bytes) {
+ byte[] decodedBytes;
+ try {
+ Class> clazz = Class.forName(JAVA_UTIL_BASE_64);
+ final Method getDecoder = clazz.getDeclaredMethod("getDecoder");
+ final Object decoder = getDecoder.invoke(null);
+ Class> decoderClazz = Class.forName("java.util.Base64$Decoder");
+ final Method decode = decoderClazz.getDeclaredMethod("decode", byte[].class);
+ decodedBytes = (byte[]) decode.invoke(decoder, bytes);
+ } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException ignored) {
+ try {
+ Class> clazz = Class.forName(ANDROID_UTIL_BASE_64);
+ final Method decode = clazz.getDeclaredMethod("decode", byte[].class, int.class);
+ int flags = 0; // android.util.Base64.DEFAULT
+ decodedBytes = (byte[]) decode.invoke(null, bytes, flags);
+ } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException ignored2) {
+ throw new NoClassDefFoundError("Base64 not available");
+ }
+ }
+
+ return decodedBytes;
}
}
diff --git a/lib/src/main/java/xyz/gianlu/librespot/core/FacebookAuthenticator.java b/lib/src/main/java/xyz/gianlu/librespot/core/FacebookAuthenticator.java
index 365c8b8e..6d29f9ec 100644
--- a/lib/src/main/java/xyz/gianlu/librespot/core/FacebookAuthenticator.java
+++ b/lib/src/main/java/xyz/gianlu/librespot/core/FacebookAuthenticator.java
@@ -33,7 +33,6 @@
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.URL;
-import java.util.Base64;
/**
* @author Gianlu
@@ -99,7 +98,7 @@ private void authData(@NotNull String json) {
credentials = Authentication.LoginCredentials.newBuilder()
.setUsername(data.get("username").getAsString())
.setTyp(Authentication.AuthenticationType.forNumber(data.get("auth_type").getAsInt()))
- .setAuthData(ByteString.copyFrom(Base64.getDecoder().decode(data.get("encoded_auth_blob").getAsString())))
+ .setAuthData(ByteString.copyFrom(Utils.fromBase64(data.get("encoded_auth_blob").getAsString())))
.build();
synchronized (credentialsLock) {
diff --git a/lib/src/main/java/xyz/gianlu/librespot/core/Session.java b/lib/src/main/java/xyz/gianlu/librespot/core/Session.java
index 0b1609b3..92518155 100644
--- a/lib/src/main/java/xyz/gianlu/librespot/core/Session.java
+++ b/lib/src/main/java/xyz/gianlu/librespot/core/Session.java
@@ -432,7 +432,7 @@ private void authenticatePartial(@NotNull Authentication.LoginCredentials creden
JsonObject obj = new JsonObject();
obj.addProperty("username", apWelcome.getCanonicalUsername());
- obj.addProperty("credentials", Utils.toBase64(reusable));
+ obj.addProperty("credentials", Utils.toBase64(reusable.toByteArray()));
obj.addProperty("type", reusableType.name());
if (inner.conf.storedCredentialsFile == null) throw new IllegalArgumentException();
@@ -883,7 +883,7 @@ public Builder() {
}
private static @NotNull Authentication.LoginCredentials decryptBlob(@NotNull String deviceId, @NotNull String username, byte[] encryptedBlob) throws GeneralSecurityException, IOException {
- encryptedBlob = Base64.getDecoder().decode(encryptedBlob);
+ encryptedBlob = Utils.fromBase64(encryptedBlob);
byte[] secret = MessageDigest.getInstance("SHA-1").digest(deviceId.getBytes());
byte[] baseKey = PBKDF2.HmacSHA1(secret, username.getBytes(), 0x100, 20);
@@ -954,7 +954,7 @@ public Builder stored(@NotNull File storedCredentials) throws IOException {
loginCredentials = Authentication.LoginCredentials.newBuilder()
.setTyp(Authentication.AuthenticationType.valueOf(obj.get("type").getAsString()))
.setUsername(obj.get("username").getAsString())
- .setAuthData(Utils.fromBase64(obj.get("credentials").getAsString()))
+ .setAuthData(ByteString.copyFrom(Utils.fromBase64(obj.get("credentials").getAsString())))
.build();
}
diff --git a/lib/src/main/java/xyz/gianlu/librespot/dealer/DealerClient.java b/lib/src/main/java/xyz/gianlu/librespot/dealer/DealerClient.java
index 1aeb237e..005ab8eb 100644
--- a/lib/src/main/java/xyz/gianlu/librespot/dealer/DealerClient.java
+++ b/lib/src/main/java/xyz/gianlu/librespot/dealer/DealerClient.java
@@ -30,6 +30,7 @@
import xyz.gianlu.librespot.common.AsyncWorker;
import xyz.gianlu.librespot.common.BytesArrayList;
import xyz.gianlu.librespot.common.NameThreadFactory;
+import xyz.gianlu.librespot.common.Utils;
import xyz.gianlu.librespot.core.ApResolver;
import xyz.gianlu.librespot.core.Session;
import xyz.gianlu.librespot.mercury.MercuryClient;
@@ -97,7 +98,7 @@ private void handleRequest(@NotNull JsonObject obj) {
Map headers = getHeaders(obj);
JsonObject payload = obj.getAsJsonObject("payload");
if ("gzip".equals(headers.get("Transfer-Encoding"))) {
- byte[] gzip = Base64.getDecoder().decode(payload.get("compressed").getAsString());
+ byte[] gzip = Utils.fromBase64(payload.get("compressed").getAsString());
try (GZIPInputStream in = new GZIPInputStream(new ByteArrayInputStream(gzip)); Reader reader = new InputStreamReader(in)) {
payload = JsonParser.parseReader(reader).getAsJsonObject();
} catch (IOException ex) {
diff --git a/player/src/main/java/xyz/gianlu/librespot/player/FileConfiguration.java b/player/src/main/java/xyz/gianlu/librespot/player/FileConfiguration.java
index c7123ece..f60bba05 100644
--- a/player/src/main/java/xyz/gianlu/librespot/player/FileConfiguration.java
+++ b/player/src/main/java/xyz/gianlu/librespot/player/FileConfiguration.java
@@ -379,7 +379,7 @@ public Session.Builder initSessionBuilder() throws IOException, GeneralSecurityE
builder.facebook();
break;
case BLOB:
- builder.blob(authUsername(), Base64.getDecoder().decode(authBlob()));
+ builder.blob(authUsername(), Utils.fromBase64(authBlob()));
break;
case USER_PASS:
builder.userPass(authUsername(), authPassword());
diff --git a/player/src/main/java/xyz/gianlu/librespot/player/StateWrapper.java b/player/src/main/java/xyz/gianlu/librespot/player/StateWrapper.java
index c92cd4f0..6d1b2be5 100644
--- a/player/src/main/java/xyz/gianlu/librespot/player/StateWrapper.java
+++ b/player/src/main/java/xyz/gianlu/librespot/player/StateWrapper.java
@@ -124,7 +124,7 @@ public static String generatePlaybackId(@NotNull Random random) {
private static String generateSessionId(@NotNull Random random) {
byte[] bytes = new byte[16];
random.nextBytes(bytes);
- return Base64.getEncoder().withoutPadding().encodeToString(bytes);
+ return Utils.toBase64NoPadding(bytes);
}
private boolean shouldPlay(@NotNull ContextTrack track) {
@@ -1536,4 +1536,4 @@ synchronized void updateMetadataFor(@NotNull String uri, @NotNull String key, @N
updateMetadataFor(index, key, value);
}
}
-}
\ No newline at end of file
+}
diff --git a/player/src/main/java/xyz/gianlu/librespot/player/state/DeviceStateHandler.java b/player/src/main/java/xyz/gianlu/librespot/player/state/DeviceStateHandler.java
index 3f62e938..5d8e7032 100644
--- a/player/src/main/java/xyz/gianlu/librespot/player/state/DeviceStateHandler.java
+++ b/player/src/main/java/xyz/gianlu/librespot/player/state/DeviceStateHandler.java
@@ -441,7 +441,7 @@ public static class CommandBody {
private CommandBody(@NotNull JsonObject obj) {
this.obj = obj;
- if (obj.has("data")) data = Base64.getDecoder().decode(obj.get("data").getAsString());
+ if (obj.has("data")) data = Utils.fromBase64(obj.get("data").getAsString());
else data = null;
if (obj.has("value")) value = obj.get("value").getAsString();