/*
* Follow the Bitcoin
* Copyright (C) 2014 Danno Ferrin
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* version 2 as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package com.shemnon.btc.coinbase;
import groovy.json.JsonSlurper;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.concurrent.Worker;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.*;
import java.nio.charset.Charset;
import java.security.GeneralSecurityException;
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.prefs.Preferences;
/**
*
* Created by shemnon on 5 Mar 2014.
*/
public class CoinBaseOAuth {
static final byte[] STORE_PASSWORD = "correct battery ".getBytes();
static final String FTM_KEY = "FTM_OAuth";
public static final String COINBASE_CLIENT_ID = System.getProperty("coinbase.client.id");
public static final String COINBASE_CLIENT_SECRET = System.getProperty("coinbase.client.secret");
public static final String CALLBACK_URL = "urn:ietf:wg:oauth:2.0:oob";
public static final String COINBASE_OAUTH_LOGIN = "https://coinbase.com/oauth/authorize?" +
"response_type=code" +
"&client_id=" + COINBASE_CLIENT_ID +
"&redirect_uri=" + CALLBACK_URL +
"&scope=addresses+transactions+user";
static final String COINBASE_AUTHORIZED_URL_ROOT = "https://coinbase.com/oauth/authorize/";
WebView browser;
StringProperty accessToken = new SimpleStringProperty();
BooleanProperty visualAuthInProgress = new SimpleBooleanProperty(false);
ExecutorService apiQueries = Executors.newSingleThreadExecutor();
public CoinBaseOAuth(WebView browser) {
this.browser = browser;
}
public String loadToken(String perm) {
Preferences node = Preferences.userNodeForPackage(CoinBaseOAuth.class);
String val = node.get(FTM_KEY+perm, null);
if (val == null) return null;
return decrypt(val);
}
public void saveToken(String value, String perm) {
Preferences node = Preferences.userNodeForPackage(CoinBaseOAuth.class);
if (value == null) {
node.remove(FTM_KEY+perm);
} else {
String val = encrypt(value);
node.put(FTM_KEY+perm, val);
}
}
private String decrypt(String val) {
try {
SecretKeySpec skeySpec = new SecretKeySpec(STORE_PASSWORD, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
byte[] encrypted = Base64.getDecoder().decode(val.getBytes());
byte[] original = cipher.doFinal(encrypted);
return new String(original, Charset.forName("US-ASCII"));
} catch (GeneralSecurityException e) {
e.printStackTrace();
}
return null; // fall through
}
private String encrypt(String key) {
String val;
try {
SecretKeySpec skeySpec = new SecretKeySpec(STORE_PASSWORD, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
byte[] encrypted = cipher.doFinal(key.getBytes());
val = new String(Base64.getEncoder().encode(encrypted));
} catch (GeneralSecurityException e) {
e.printStackTrace();
val = null;
}
return val;
}
public void clearTokens() {
saveToken(null, "refresh_token");
saveToken(null, "access_token");
saveToken(null, "expire");
Platform.runLater(() -> accessToken.setValue(""));
}
/**
*
*
* @param attemptRefresh If tokens are out of date, attempt a refresh
* @param attemptLogin If the tokens are invalid and refresh fails, attempt login
* @return true if tokens are immediatly valid.
*/
public boolean checkTokens(boolean attemptRefresh, boolean attemptLogin) {
boolean success = false;
do {
try {
long time;
String xp = loadToken("expire");
if (xp == null) break;
time = Long.parseLong(xp);
if (System.currentTimeMillis() < time) {
Platform.runLater(() -> accessToken.setValue(loadToken("access_token")));
success = true;
} else {
String refreshToken = loadToken("refresh_token");
if (refreshToken != null) {
success = attemptRefresh && refreshTokens(refreshToken);
}
}
} catch (Throwable t) {
Platform.runLater(() -> accessToken.setValue(""));
t.printStackTrace();
break;
}
} while (false);
if (!success) {
Platform.runLater(() -> accessToken.setValue(""));
if (attemptLogin) {
requestLogin();
}
}
return success;
}
public boolean refreshTokens(String refreshToken) {
if (refreshToken == null) {
return false;
}
String postContent = "";
try {
URL tokenURL = new URL("https://coinbase.com/oauth/token");
HttpURLConnection uc = (HttpURLConnection) tokenURL.openConnection();
uc.setRequestMethod("POST");
uc.setDoOutput(true);
postContent = "grant_type=refresh_token" +
"&refresh_token=" + refreshToken +
"&redirect_uri=" + CALLBACK_URL +
"&client_id=" + COINBASE_CLIENT_ID +
"&client_secret=" + COINBASE_CLIENT_SECRET;
uc.getOutputStream().write(postContent.getBytes());
uc.connect();
JsonSlurper slurper = new JsonSlurper();
Map m = (Map) slurper.parse(new InputStreamReader(uc.getInputStream())); // TODO cleanup
long expire = System.currentTimeMillis() + ((((Number)m.get("expires_in")).longValue()*1000));
saveToken((String) m.get("access_token"), "access_token");
saveToken((String) m.get("refresh_token"), "refresh_token");
saveToken(Long.toString(expire), "expire");
Platform.runLater(() -> accessToken.setValue((String) m.get("access_token")));
return true;
} catch (IOException e) {
Platform.runLater(() -> accessToken.setValue(""));
System.out.println(postContent);
e.printStackTrace();
}
return false;
}
public void requestLogin() {
visualAuthInProgress.set(true);
WebEngine webEngine = browser.getEngine();
webEngine.getLoadWorker().stateProperty().addListener((ov, oldValue, newValue) -> {
if (newValue == Worker.State.SUCCEEDED) {
String location = webEngine.getLocation();
if (location.startsWith(COINBASE_AUTHORIZED_URL_ROOT)) {
String code = location.substring(COINBASE_AUTHORIZED_URL_ROOT.length());
apiQueries.submit(() -> {
requestTokens(code);
Platform.runLater(() -> visualAuthInProgress.set(false));
});
webEngine.setOnStatusChanged(null);
}
}
});
webEngine.load(COINBASE_OAUTH_LOGIN);
}
private void requestTokens(String code) {
try {
URL tokenURL = new URL("https://coinbase.com/oauth/token");
HttpURLConnection uc = (HttpURLConnection) tokenURL.openConnection();
uc.setRequestMethod("POST");
uc.setDoOutput(true);
String postContent = "grant_type=authorization_code" +
"&code=" + code +
"&redirect_uri=" + CALLBACK_URL +
"&client_id=" + COINBASE_CLIENT_ID +
"&client_secret=" + COINBASE_CLIENT_SECRET;
uc.getOutputStream().write(postContent.getBytes());
uc.connect();
JsonSlurper slurper = new JsonSlurper();
Map m = (Map) slurper.parse(new InputStreamReader(uc.getInputStream())); // TODO cleanup
long expire = System.currentTimeMillis() + ((((Number)m.get("expires_in")).longValue()*1000));
saveToken((String) m.get("access_token"), "access_token");
saveToken((String) m.get("refresh_token"), "refresh_token");
saveToken(Long.toString(expire), "expire");
Platform.runLater(() -> accessToken.setValue((String) m.get("access_token")));
} catch (Exception e) {
Platform.runLater(() -> accessToken.setValue(""));
e.printStackTrace();
}
}
public String getAccessToken() {
return accessToken.get();
}
public void setAccessToken(String newAccessToken) {
Platform.runLater(() -> accessToken.setValue(newAccessToken));
}
public StringProperty accessTokenProperty() {
return accessToken;
}
public boolean getVisualAuthInProgress() {
return visualAuthInProgress.get();
}
public BooleanProperty visualAuthInProgressProperty() {
return visualAuthInProgress;
}
public void setVisualAuthInProgress(boolean visualAuthInProgress) {
this.visualAuthInProgress.set(visualAuthInProgress);
}
}