// 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()); } }