From 9bd3a06695e5ad2f4779e8c2dd12af0f0ccf711d Mon Sep 17 00:00:00 2001 From: Alix von Schirp Date: Tue, 24 Jan 2023 18:51:18 +0100 Subject: [PATCH] Implemented saving and fetching Credentials closes #4 --- .../LastFMToSpotify.java | 46 +++++++------ .../arguments/ArgumentHandler.java | 9 +++ .../arguments/Arguments.java | 4 +- .../util/CryptoHelper.java | 64 +++++++++++++++++++ .../util/TokenHelper.java | 17 +++-- 5 files changed, 114 insertions(+), 26 deletions(-) create mode 100644 src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/util/CryptoHelper.java diff --git a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java index a1bdcf5..c32d43e 100644 --- a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java +++ b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java @@ -29,6 +29,7 @@ import static de.b00tload.tools.lastfmtospotifyplaylist.util.Logger.logLn; public class LastFMToSpotify { public static final String LINE_SEPERATOR = System.getProperty("line.separator"); + public static final String USER_HOME = System.getProperty("user.home"); public static HashMap configuration; public static void main(String[] args) { @@ -67,7 +68,6 @@ public class LastFMToSpotify { } } - try { logLn("Authenticating with Spotify...", 1); SpotifyApi.Builder build = SpotifyApi.builder(); @@ -76,23 +76,33 @@ public class LastFMToSpotify { build.setRedirectUri(URI.create("http://localhost:9876/callback/spotify/")); SpotifyApi api = build.build(); AtomicBoolean waiting = new AtomicBoolean(true); - try (Javalin webserver = Javalin.create().start(9876)) { - Runtime.getRuntime().addShutdownHook(new Thread(webserver::stop)); - webserver.get("/callback/spotify", ctx -> { - if(ctx.queryParamMap().containsKey("code")) { - AuthorizationCodeCredentials cred = api.authorizationCode(ctx.queryParam("code")).build().execute(); - configuration.put("spotify.access", cred.getAccessToken()); - if(configuration.containsKey("spotify.saveaccess")) TokenHelper.saveTokens(cred); - ctx.result("success. ").contentType(ContentType.TEXT_HTML).status(HttpStatus.OK); - waiting.set(false); - } else { - logLn("Error: Spotify authorization failed."+LINE_SEPERATOR+ctx.queryParam("error"), 1); - System.exit(500); - } - }); - logLn("Waiting for Spotify authorization.", 1); - //TODO: Open auth page in Browser - while (waiting.get()); + if (configuration.containsKey("cache.crypto") && TokenHelper.existsTokens()) { + AuthorizationCodeCredentials oldcred = TokenHelper.fetchTokens(); + AuthorizationCodeCredentials newcred = api.authorizationCodeRefresh(api.getClientId(), api.getClientSecret(), oldcred.getRefreshToken()).build().execute(); + TokenHelper.saveTokens(newcred); + configuration.put("spotify.access", newcred.getAccessToken()); + } else { + try (Javalin webserver = Javalin.create().start(9876)) { + Runtime.getRuntime().addShutdownHook(new Thread(webserver::stop)); +// webserver.exception(Exception.class, (exception, ctx) -> { +// ctx.result(exception.getMessage()); +// }); + webserver.get("/callback/spotify", ctx -> { + if(ctx.queryParamMap().containsKey("code")) { + AuthorizationCodeCredentials cred = api.authorizationCode(ctx.queryParam("code")).build().execute(); + configuration.put("spotify.access", cred.getAccessToken()); + if(configuration.containsKey("cache.crypto")) TokenHelper.saveTokens(cred); + ctx.result("success. ").contentType(ContentType.TEXT_HTML).status(HttpStatus.OK); + waiting.set(false); + } else { + logLn("Error: Spotify authorization failed."+LINE_SEPERATOR+ctx.queryParam("error"), 1); + System.exit(500); + } + }); + logLn("Waiting for Spotify authorization.", 1); + //TODO: Open auth page in Browser + while (waiting.get()); + } } logLn("Authenticating with LastFM...", 1); Caller.getInstance().setApiRootUrl("https://ws.audioscrobbler.com/2.0/"); diff --git a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/arguments/ArgumentHandler.java b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/arguments/ArgumentHandler.java index 0200f36..58c690e 100644 --- a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/arguments/ArgumentHandler.java +++ b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/arguments/ArgumentHandler.java @@ -41,6 +41,7 @@ public class ArgumentHandler { case NAME -> name(value); case PUBLIC -> access("public"); case COLLABORATIVE -> access("collaborative"); + case SPOTIFY_CACHING -> cache(value); } } @@ -229,4 +230,12 @@ public class ArgumentHandler { configuration.put("playlist.name", name); } + + public static void cache(String value){ + if (value == null || value.isEmpty()) { + logLn("--spotifycache must be provided with a password. Check usage: " + Arguments.SPOTIFY_CACHING.getUsage(), 1); + System.exit(500); + } + configuration.put("cache.crypto", value); + } } diff --git a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/arguments/Arguments.java b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/arguments/Arguments.java index 88e0a4c..396e71d 100644 --- a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/arguments/Arguments.java +++ b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/arguments/Arguments.java @@ -41,7 +41,9 @@ public enum Arguments { PUBLIC("public", "[Optional] [EXCLUSIVE: public, collaborative]" + LINE_SEPERATOR + "Makes the playlist public.", "--public", "pP"), COLLABORATIVE("collaborative", "[Optional] [EXCLUSIVE: public, collaborative]" + LINE_SEPERATOR - + "Makes the playlist collaborative.", "--collaborative", "pC"); + + "Makes the playlist collaborative.", "--collaborative", "pC"), + SPOTIFY_CACHING("spotifycache", "[Optional]" + LINE_SEPERATOR + + "Saves and (if possible) retrieves auth tokens from a locally saved file.", "--spotifycache ", "sTk"); private final String name; private final String description; diff --git a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/util/CryptoHelper.java b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/util/CryptoHelper.java new file mode 100644 index 0000000..4339930 --- /dev/null +++ b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/util/CryptoHelper.java @@ -0,0 +1,64 @@ +package de.b00tload.tools.lastfmtospotifyplaylist.util; + + +import javax.crypto.*; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import java.io.*; +import java.nio.file.Path; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; + +public class CryptoHelper { + + public static SecretKey createKeyFromPassword(String pass){ + try { + KeySpec spec = new PBEKeySpec(pass.toCharArray(), "abcdefghijklmnop".getBytes(), 65536, 256); // AES-256 + SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + byte[] key = new byte[0]; + key = f.generateSecret(spec).getEncoded(); + return new SecretKeySpec(key, "AES"); + } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + public static void serializeEncrypted(Serializable obj, Path file, SecretKey key){ + try { + if(file.toFile().exists()) file.toFile().delete(); + if(!file.getParent().toFile().exists()) file.getParent().toFile().mkdirs(); + file.toFile().createNewFile(); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + IvParameterSpec iv = new IvParameterSpec("abcdefghijklmnop".getBytes()); + cipher.init(Cipher.ENCRYPT_MODE, key, iv); + SealedObject sealedObject = new SealedObject( obj, cipher); + CipherOutputStream cipherOutputStream = new CipherOutputStream( new BufferedOutputStream( new FileOutputStream( file.toFile() ) ), cipher ); + ObjectOutputStream outputStream = new ObjectOutputStream( cipherOutputStream ); + outputStream.writeObject( sealedObject ); + outputStream.close(); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IOException | IllegalBlockSizeException | InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); + } + } + + public static Serializable deserializeEncrypted(Path file, SecretKey key) { + Serializable ret = null; + try { + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + IvParameterSpec iv = new IvParameterSpec("abcdefghijklmnop".getBytes()); + cipher.init(Cipher.DECRYPT_MODE, key, iv); + CipherInputStream cipherInputStream = new CipherInputStream( new BufferedInputStream( new FileInputStream( file.toFile() ) ), cipher ); + ObjectInputStream inputStream = new ObjectInputStream( cipherInputStream ); + SealedObject sealedObject = (SealedObject) inputStream.readObject(); + ret = (Serializable) sealedObject.getObject(cipher); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IOException | IllegalBlockSizeException | ClassNotFoundException | BadPaddingException | InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); + } + return ret; + } + +} diff --git a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/util/TokenHelper.java b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/util/TokenHelper.java index 8dfd3d2..8197b21 100644 --- a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/util/TokenHelper.java +++ b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/util/TokenHelper.java @@ -2,18 +2,21 @@ package de.b00tload.tools.lastfmtospotifyplaylist.util; import se.michaelthelin.spotify.model_objects.credentials.AuthorizationCodeCredentials; +import java.nio.file.Path; + +import static de.b00tload.tools.lastfmtospotifyplaylist.LastFMToSpotify.USER_HOME; +import static de.b00tload.tools.lastfmtospotifyplaylist.LastFMToSpotify.configuration; + public class TokenHelper { public static void saveTokens(AuthorizationCodeCredentials cred) { - //TODO: Save tokens + CryptoHelper.serializeEncrypted(cred, Path.of(USER_HOME, "/.lfm2s/spotify.lfm2scred"), CryptoHelper.createKeyFromPassword(configuration.get("cache.crypto"))); } - public static String getAccessToken(){ - //TODO: Read AccessToken - return ""; + public static AuthorizationCodeCredentials fetchTokens() { + return (AuthorizationCodeCredentials) CryptoHelper.deserializeEncrypted(Path.of(USER_HOME, "/.lfm2s/spotify.lfm2scred"), CryptoHelper.createKeyFromPassword(configuration.get("cache.crypto"))); } - public static String getRefreshToken(){ - //TODO: Read RefreshToken - return ""; + public static boolean existsTokens(){ + return Path.of(USER_HOME, "/.lfm2s/spotify.lfm2scred").toFile().exists(); } }