// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.npm;
import java.awt.Component;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.net.Authenticator.RequestorType;
import java.net.PasswordAuthentication;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.CRC32;
import javax.swing.text.html.HTMLEditorKit;
import org.netbeans.spi.keyring.KeyringProvider;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.oauth.OAuthToken;
import org.openstreetmap.josm.gui.preferences.server.ProxyPreferencesPanel;
import org.openstreetmap.josm.gui.widgets.HtmlPanel;
import org.openstreetmap.josm.io.OsmApi;
import org.openstreetmap.josm.io.auth.AbstractCredentialsAgent;
import org.openstreetmap.josm.io.auth.CredentialsAgentException;
import org.openstreetmap.josm.tools.Utils;
public class NPMCredentialsAgent extends AbstractCredentialsAgent {
private KeyringProvider provider;
private NPMType type;
/**
* Cache the results since there might be pop ups and password prompts from
* the native manager. This can get annoying, if it shows too often.
*
* Yes, there is another cache in AbstractCredentialsAgent. It is used
* to avoid prompting the user for login multiple times in one session,
* when they decide not to save the credentials.
* In contrast, this cache avoids read request the backend in general.
*/
private Map<RequestorType, PasswordAuthentication> credentialsCache = new HashMap<>();
private OAuthToken oauthCache;
public NPMCredentialsAgent(NPMType type) {
this.type = type;
}
private KeyringProvider getProvider() {
if (provider == null) {
provider = type.getProvider();
}
return provider;
}
protected String getServerDescriptor() {
String pref = Main.pref.getPreferenceFile().getAbsolutePath();
String url = Main.pref.get("osm-server.url", null);
if (url == null) {
url = OsmApi.DEFAULT_API_URL;
}
CRC32 id = new CRC32();
id.update((pref+"/"+url).getBytes());
String hash = Integer.toHexString((int)id.getValue());
return "JOSM.native-password-manager-plugin.api."+hash;
}
protected String getProxyDescriptor() {
String pref = Main.pref.getPreferenceFile().getAbsolutePath();
String host = Main.pref.get(ProxyPreferencesPanel.PROXY_HTTP_HOST, "");
String port = Main.pref.get(ProxyPreferencesPanel.PROXY_HTTP_PORT, "");
CRC32 id = new CRC32();
id.update((pref+"/"+host+"/"+port).getBytes());
String hash = Integer.toHexString((int)id.getValue());
return "JOSM.native-password-manager-plugin.proxy."+hash;
}
protected String getOAuthDescriptor() {
String pref = Main.pref.getPreferenceFile().getAbsolutePath();
// TODO: put more identifying data here
CRC32 id = new CRC32();
id.update((pref).getBytes());
String hash = Integer.toHexString((int)id.getValue());
return "JOSM.native-password-manager-plugin.oauth."+hash;
}
@Override
public PasswordAuthentication lookup(RequestorType rt, String host) throws CredentialsAgentException {
PasswordAuthentication cache = credentialsCache.get(rt);
if (cache != null)
return cache;
String user;
char[] password;
PasswordAuthentication auth;
switch(rt) {
case SERVER:
if(OsmApi.getOsmApi().getHost().equals(host)) {
user = stringNotNull(getProvider().read(getServerDescriptor()+".username"));
password = getProvider().read(getServerDescriptor()+".password");
} else {
user = stringNotNull(getProvider().read(host+".username"));
password = getProvider().read(host+".password");
}
auth = new PasswordAuthentication(user, password == null ? new char[0] : password);
break;
case PROXY:
user = stringNotNull(getProvider().read(getProxyDescriptor()+".username"));
password = getProvider().read(getProxyDescriptor()+".password");
auth = new PasswordAuthentication(user, password == null ? new char[0] : password);
break;
default: throw new IllegalStateException();
}
credentialsCache.put(rt, auth);
return auth;
}
@Override
public void store(RequestorType rt, String host, PasswordAuthentication credentials) throws CredentialsAgentException {
char[] username, password;
if (credentials == null) {
username = null;
password = null;
} else {
username = credentials.getUserName() == null ? null : credentials.getUserName().toCharArray();
password = credentials.getPassword();
}
if (username != null && username.length == 0) {
username = null;
}
// password could be empty string in theory, so don't set to null if empty
String prefix, usernameDescription, passwordDescription;
switch(rt) {
case SERVER:
if(OsmApi.getOsmApi().getHost().equals(host)) {
prefix = getServerDescriptor();
usernameDescription = tr("JOSM/OSM API/Username");
passwordDescription = tr("JOSM/OSM API/Password");
} else {
prefix = host;
usernameDescription = tr("{0}/Username", host);
passwordDescription = tr("{0}/Password", host);
}
break;
case PROXY:
prefix = getProxyDescriptor();
usernameDescription = tr("JOSM/Proxy/Username");
passwordDescription = tr("JOSM/Proxy/Password");
break;
default: throw new IllegalStateException();
}
if (username == null) {
getProvider().delete(prefix+".username");
getProvider().delete(prefix+".password");
credentialsCache.remove(rt);
} else {
getProvider().save(prefix+".username", username, usernameDescription);
if (password == null) {
getProvider().delete(prefix+".password");
} else {
getProvider().save(prefix+".password", password, passwordDescription);
}
credentialsCache.put(rt, new PasswordAuthentication(stringNotNull(username), password));
}
}
@Override
public OAuthToken lookupOAuthAccessToken() throws CredentialsAgentException {
if (oauthCache != null)
return oauthCache;
String prolog = getOAuthDescriptor();
char[] key = getProvider().read(prolog+".key");
char[] secret = getProvider().read(prolog+".secret");
return new OAuthToken(stringNotNull(key), stringNotNull(secret));
}
@Override
public void storeOAuthAccessToken(OAuthToken oat) throws CredentialsAgentException {
String key, secret;
if (oat == null) {
key = null;
secret = null;
}
else {
key = oat.getKey();
secret = oat.getSecret();
}
String prolog = getOAuthDescriptor();
if (key == null || key.isEmpty() || secret == null || secret.isEmpty()) {
getProvider().delete(prolog+".key");
getProvider().delete(prolog+".secret");
oauthCache = null;
} else {
getProvider().save(prolog+".key", key.toCharArray(), tr("JOSM/OAuth/OSM API/Key"));
getProvider().save(prolog+".secret", secret.toCharArray(), tr("JOSM/OAuth/OSM API/Secret"));
oauthCache = new OAuthToken(key, secret);
}
}
private static String stringNotNull(char[] charData) {
if (charData == null)
return "";
return String.valueOf(charData);
}
@Override
public Component getPreferencesDecorationPanel() {
HtmlPanel pnlMessage = new HtmlPanel();
HTMLEditorKit kit = (HTMLEditorKit)pnlMessage.getEditorPane().getEditorKit();
kit.getStyleSheet().addRule(".warning-body {background-color:rgb(253,255,221);padding: 10pt; border-color:rgb(128,128,128);border-style: solid;border-width: 1px;}");
StringBuilder text = new StringBuilder();
text.append("<html><body>"
+ "<p class=\"warning-body\">"
+ "<strong>"+tr("Native Password Manager Plugin")+"</strong><br>"
+ tr("The username and password is protected by {0}.", type.getName())
);
List<String> sensitive = new ArrayList<>();
if (Main.pref.get("osm-server.username", null) != null) {
sensitive.add(tr("username"));
}
if (Main.pref.get("osm-server.password", null) != null) {
sensitive.add(tr("password"));
}
if (Main.pref.get(ProxyPreferencesPanel.PROXY_USER, null) != null) {
sensitive.add(tr("proxy username"));
}
if (Main.pref.get(ProxyPreferencesPanel.PROXY_PASS, null) != null) {
sensitive.add(tr("proxy password"));
}
if (Main.pref.get("oauth.access-token.key", null) != null) {
sensitive.add(tr("oauth key"));
}
if (Main.pref.get("oauth.access-token.secret", null) != null) {
sensitive.add(tr("oauth secret"));
}
if (!sensitive.isEmpty()) {
text.append(tr("<br><strong>Warning:</strong> There may be sensitive data left in your preference file. ({0})", Utils.join(", ", sensitive)));
}
pnlMessage.setText(text.toString());
return pnlMessage;
}
@Override
public String getSaveUsernameAndPasswordCheckboxText() {
return tr("Save user and password ({0})", type.getName());
}
}