/* * Copyright 2011 Luke Usherwood. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 2.1 of the License, or * (at your option) any later version. * * 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package net.bettyluke.tracinstant.prefs; import java.awt.Rectangle; import java.nio.charset.Charset; import java.security.Key; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.SecretKeySpec; import net.bettyluke.util.AppProperties; public final class TracInstantProperties { private TracInstantProperties() {} private static final String TRAC_PWD = "TracPwd"; private static final String TRAC_USERNAME = "TracUser"; private static final int MAX_MRU = 8; private static final String TRAC_REMEMBER_PASSWORD = "TracRmbrPwd"; private static final Cipher CIPHER; static { Cipher c; try { c = Cipher.getInstance("AES"); // Required to be valid in the Java platform } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { e.printStackTrace(); c = null; } CIPHER = c; } private static final AtomicReference<AppProperties> s_SharedInstance = new AtomicReference<>(); public static void initialise(String companyName, String appName) { s_SharedInstance.set(new AppProperties(companyName, appName)); s_SharedInstance.get().loadProperties(); } public static AppProperties get() { return s_SharedInstance.get(); } public static String getUsername() { return get().getString(TRAC_USERNAME, ""); } public static void addUsername(String username) { get().putString(TRAC_USERNAME, username); } public static void addRememberPassword(boolean remember) { get().putBoolean(TRAC_REMEMBER_PASSWORD, remember); } public static boolean getRememberPassword() { return get().getBoolean(TRAC_REMEMBER_PASSWORD, false); } /** * Whether the application /supported/ remembering passwords at the time the properties file * was written. This is just used to ease upgrade of people to the new version by performing * a one-time prompt (rather than defaulting to a blank password and getting a server error.) */ public static boolean hasPasswordSupport() { return get().getValue(TRAC_REMEMBER_PASSWORD) != null; } public static String getPassword() { String password = get().getString(TRAC_PWD, ""); password = transform(password, Cipher.DECRYPT_MODE); return password; } public static void addPassword(String password) { password = transform(password, Cipher.ENCRYPT_MODE); get().putString(TRAC_PWD, password); } private static String transform(String str, int mode) { if (str.isEmpty()) { return str; } Charset utf8 = Charset.forName("UTF-8"); // 16 chars needed. try { Key key = new SecretKeySpec( (TRAC_REMEMBER_PASSWORD+TRAC_REMEMBER_PASSWORD).substring(0, 16) .getBytes("UTF-8"), "AES"); CIPHER.init(mode, key); if (mode == Cipher.ENCRYPT_MODE) { CIPHER.init(Cipher.ENCRYPT_MODE, key); byte[] encryptedVal = CIPHER.doFinal(str.getBytes(utf8)); byte[] encodedValue = Base64.getEncoder().encode(encryptedVal); return new String(encodedValue, utf8); } else { CIPHER.init(Cipher.DECRYPT_MODE, key); byte[] decodedValue = Base64.getDecoder().decode(str.getBytes(utf8)); byte[] decryptedVal = CIPHER.doFinal(decodedValue); return new String(decryptedVal, utf8); } // NB: this broad exception handling also catches 'IllegalArgumentException' // which is a RuntimeException that could occur from Base64 coding if the encryption // key ever changes. Must not let that out to crash the caller code. } catch (Exception e) { e.printStackTrace(); return ""; } } public static String getURL() { List<String> urlList = getURL_MRU(); return urlList.isEmpty() ? "" : urlList.get(0); } public static List<String> getURL_MRU() { return getStringList("TracURL_MRU", "[https://trac.edgewall.org]"); } /** Whether the option to cache data was selected. */ public static boolean getUseCache() { return get().getBoolean("CacheSlurpedData", true); } public static void setUseCache(boolean bool) { get().putBoolean("CacheSlurpedData", bool); } public static boolean getActiveTicketsOnly() { return TracInstantProperties.get().getBoolean("FetchActiveTicketsOnly", false); } public static void setActiveTicketsOnly(boolean b) { TracInstantProperties.get().putBoolean("FetchActiveTicketsOnly", b); } public static void addURL_MRU(String urlText) { addMRU("TracURL_MRU", urlText); } public static String getAttachmentsDir() { List<String> dirList = getAttachmentsDir_MRU(); return dirList.isEmpty() ? "" : dirList.get(0); } public static List<String> getAttachmentsDir_MRU() { return getStringList("AttachmentsDir_MRU", ""); } public static void addAttachmentsDir_MRU(String dir) { addMRU("AttachmentsDir_MRU", dir); } public static void putStringList(String key, List<String> list) { get().putString(key, list.toString()); } public static List<String> getStringList(String key, String defaultStringList) { List<String> result = new ArrayList<>(); String strList = get().getValue(key); if (strList == null) { strList = defaultStringList; if (strList.length() > 2) { get().putString(key, defaultStringList); } } if (strList.startsWith("[") && strList.endsWith("]")) { String[] strs = strList.substring(1, strList.length() - 1).split(","); for (String s : strs) { result.add(s.trim()); } } return result; } private static void addMRU(String key, String value) { List<String> list = getStringList(key, ""); while (list.remove(value)) { // carry on } list.add(0, value); if (list.size() > MAX_MRU) { list.remove(list.size() - 1); } putStringList(key, list); } public static Rectangle getRectangle(String property, Rectangle defaultValue) { String encodedString = get().getValue(property); try { Map<String, String> map = parseKeyValuePairs(encodedString); Rectangle r = new Rectangle(); r.x = Integer.valueOf(map.get("x")); r.y = Integer.valueOf(map.get("y")); r.width = Integer.valueOf(map.get("width")); r.height = Integer.valueOf(map.get("height")); return r; } catch (ParseFailed | NumberFormatException ex) { } return defaultValue; } private static final class ParseFailed extends Exception {} private static Map<String, String> parseKeyValuePairs(String str) throws ParseFailed { if (str == null || str.isEmpty()) { throw new ParseFailed(); } if (str.endsWith("]")) { int bracket = str.indexOf('['); if (bracket == -1) { throw new ParseFailed(); } str = str.substring(bracket + 1, str.length() - 1); } String[] pairs = str.split(","); Map<String, String> result = new HashMap<>(pairs.length); for (String pair : pairs) { String[] keyValue = pair.split("="); if (keyValue.length != 2) { throw new ParseFailed(); } result.put(keyValue[0].trim(), keyValue[1].trim()); } return result; } }