diff --git a/src/main/java/com/socrata/datasync/config/userpreferences/CryptoUtil.java b/src/main/java/com/socrata/datasync/config/userpreferences/CryptoUtil.java new file mode 100644 index 00000000..985a8791 --- /dev/null +++ b/src/main/java/com/socrata/datasync/config/userpreferences/CryptoUtil.java @@ -0,0 +1,112 @@ +package com.socrata.datasync.config.userpreferences; + +import javax.crypto.*; +import javax.crypto.spec.*; +import javax.crypto.NoSuchPaddingException; +import java.security.SecureRandom; +import java.security.NoSuchAlgorithmException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.nio.charset.StandardCharsets; +import java.io.*; +import java.util.Arrays; +import java.util.prefs.Preferences; + +import org.apache.commons.net.util.Base64; + +final class CryptoUtil { + private static final byte[] obfuscatedPrefix; + private static final byte[] key; + + static { + int[] ints = + // SOCRATAOBFUS in control characters + new int[] { 0x13, 0x0f, 0x03, 0x12, 0x01, 0x14, 0x01, 0x0f, 0x02, 0x06, 0x15, 0x13, 0x0a }; + byte[] bytes = new byte[ints.length]; + for(int i = 0; i != ints.length; ++i) { + bytes[i] = (byte) ints[i]; + } + obfuscatedPrefix = bytes; + + Preferences p = Preferences.userRoot().node("SocrataIntegrationPrefs"); + bytes = p.getByteArray("junk", null); + if(bytes == null || bytes.length != 16) { + bytes = new byte[16]; + new SecureRandom().nextBytes(bytes); + p.putByteArray("junk", bytes); + } + key = bytes; + } + + private static byte[] writeIv(OutputStream os) throws IOException { + byte[] result = new byte[16]; + new SecureRandom().nextBytes(result); + os.write(result); + return result; + } + + private static byte[] readIv(InputStream is) throws IOException { + byte[] result = new byte[16]; + int off = 0; + while(off < result.length) { + int count = is.read(result, off, result.length - off); + if(count == -1) throw new EOFException(); + else off += count; + } + return result; + } + + private static Cipher cipher(int mode, byte[] iv) throws NoSuchAlgorithmException, InvalidKeyException, NoSuchPaddingException, InvalidAlgorithmParameterException { + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + + cipher.init(mode, new SecretKeySpec(Arrays.copyOf(key, key.length), "AES"), new IvParameterSpec(iv)); + return cipher; + } + + public static String obfuscate(String s) { + if(s == null) return null; + + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(obfuscatedPrefix); + + try(CipherOutputStream cos = new CipherOutputStream(baos, cipher(Cipher.ENCRYPT_MODE, writeIv(baos))); + OutputStreamWriter osw = new OutputStreamWriter(cos, StandardCharsets.UTF_8)) { + osw.write(s); + } + + return Base64.encodeBase64URLSafeString(baos.toByteArray()); + } catch(Exception e) { + throw new RuntimeException("Unexpected error", e); + } + } + + public static String deobfuscate(String s, String ifInvalid) { + if(s == null) return null; + + try { + byte[] bytes = Base64.decodeBase64(s); + if(bytes.length >= obfuscatedPrefix.length && Arrays.equals(obfuscatedPrefix, Arrays.copyOfRange(bytes, 0, obfuscatedPrefix.length))) { + bytes = Arrays.copyOfRange(bytes, obfuscatedPrefix.length, bytes.length); + try(ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + CipherInputStream cis = new CipherInputStream(bais, cipher(Cipher.DECRYPT_MODE, readIv(bais))); + InputStreamReader isr = new InputStreamReader(cis, StandardCharsets.UTF_8)) { + StringBuilder sb = new StringBuilder(); + int c; + while((c = isr.read()) != -1) { + sb.append((char) c); + } + return sb.toString(); + } + } else { + return s; + } + } catch(IOException e) { + return ifInvalid; + } catch(IndexOutOfBoundsException e) { + return ifInvalid; + } catch(Exception e) { + throw new RuntimeException("Unexpected error", e); + } + } +} diff --git a/src/main/java/com/socrata/datasync/config/userpreferences/UserPreferencesFile.java b/src/main/java/com/socrata/datasync/config/userpreferences/UserPreferencesFile.java index 38dc8cc5..1faf932d 100644 --- a/src/main/java/com/socrata/datasync/config/userpreferences/UserPreferencesFile.java +++ b/src/main/java/com/socrata/datasync/config/userpreferences/UserPreferencesFile.java @@ -3,6 +3,7 @@ import com.socrata.datasync.SocrataConnectionInfo; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import java.util.List; @@ -53,6 +54,16 @@ public String getUsername() { } @JsonProperty("password") + public String getPasswordObfuscated() { + return CryptoUtil.obfuscate(password); + } + + @JsonProperty("password") + public void setPasswordObfuscated(String pwd) { + password = CryptoUtil.deobfuscate(pwd, null); + } + + @JsonIgnore public String getPassword() { return password; } @@ -73,6 +84,11 @@ public String getProxyUsername() { } @JsonProperty("proxyPassword") + public String getProxyPasswordObfuscated() { + return CryptoUtil.obfuscate(proxyPassword); + } + + @JsonIgnore public String getProxyPassword() { return proxyPassword; } @@ -113,6 +129,16 @@ public String getSmtpUsername() { } @JsonProperty("smtpPassword") + public String getSmtpPasswordObfuscated() { + return CryptoUtil.obfuscate(smtpPassword); + } + + @JsonProperty("smtpPassword") + public void setSmtpPasswordObfuscated(String pwd) { + smtpPassword = CryptoUtil.deobfuscate(pwd, null); + } + + @JsonIgnore public String getSmtpPassword() { return smtpPassword; } @@ -131,6 +157,9 @@ public String getNumRowsPerChunk() { public void setProxyUsername(String username) { proxyUsername = username; } @JsonProperty("proxyPassword") + public void setProxyPasswordObfuscated(String password) { proxyPassword = CryptoUtil.deobfuscate(password, null); } + + @JsonIgnore public void setProxyPassword(String password) { proxyPassword = password; } @JsonProperty("defaultTimeFormats") diff --git a/src/main/java/com/socrata/datasync/config/userpreferences/UserPreferencesJava.java b/src/main/java/com/socrata/datasync/config/userpreferences/UserPreferencesJava.java index dd6a99b9..7e187a50 100644 --- a/src/main/java/com/socrata/datasync/config/userpreferences/UserPreferencesJava.java +++ b/src/main/java/com/socrata/datasync/config/userpreferences/UserPreferencesJava.java @@ -72,7 +72,7 @@ public void saveUsername(String username) { } public void savePassword(String password) { - saveKeyValuePair(PASSWORD, password); + saveKeyValuePair(PASSWORD, CryptoUtil.obfuscate(password)); } public void saveProxyHost(String host) { saveKeyValuePair(PROXY_HOST, host); } @@ -84,7 +84,7 @@ public void saveProxyPort(String port) { public void saveProxyUsername(String username) { saveKeyValuePair(PROXY_USERNAME, username); } public void saveProxyPassword(String password) { - saveKeyValuePair(PROXY_PASSWORD, password); + saveKeyValuePair(PROXY_PASSWORD, CryptoUtil.obfuscate(password)); } public void saveAdminEmail(String adminEmail) { saveKeyValuePair(ADMIN_EMAIL, adminEmail); } @@ -123,7 +123,7 @@ public void saveSMTPUsername(String username) { } public void saveSMTPPassword(String password) { - saveKeyValuePair(SMTP_PASSWORD, password); + saveKeyValuePair(SMTP_PASSWORD, CryptoUtil.obfuscate(password)); } public void saveDefaultTimeFormats(List defaultTimeFormats) { @@ -148,7 +148,10 @@ public String getUsername() { } public String getPassword() { - return userPrefs.get(PASSWORD, ""); + String base = userPrefs.get(PASSWORD, ""); + String result = CryptoUtil.deobfuscate(base, ""); + if(base == result) savePassword(result); + return result; } public String getProxyHost() { return userPrefs.get(PROXY_HOST, null); } @@ -162,7 +165,10 @@ public String getProxyUsername() { } public String getProxyPassword() { - return userPrefs.get(PROXY_PASSWORD, null); + String base = userPrefs.get(PROXY_PASSWORD, null); + String result = CryptoUtil.deobfuscate(base, null); + if(base == result) saveProxyPassword(result); + return result; } public String getAdminEmail() { @@ -195,7 +201,10 @@ public String getSmtpUsername() { } public String getSmtpPassword() { - return userPrefs.get(SMTP_PASSWORD, ""); + String base = userPrefs.get(SMTP_PASSWORD, ""); + String result = CryptoUtil.deobfuscate(base, ""); + if(base == result) saveSMTPPassword(result); + return result; } public String getFilesizeChunkingCutoffMB() { diff --git a/src/test/java/com/socrata/datasync/config/userpreferences/CryptoUtilTest.java b/src/test/java/com/socrata/datasync/config/userpreferences/CryptoUtilTest.java new file mode 100644 index 00000000..e821bcdd --- /dev/null +++ b/src/test/java/com/socrata/datasync/config/userpreferences/CryptoUtilTest.java @@ -0,0 +1,13 @@ +package com.socrata.datasync.config.userpreferences; + +import junit.framework.TestCase; +import org.junit.Test; + +public class CryptoUtilTest { + @Test + public void testObfuscationRoundtrips() { + for(String x : new String[] { "hello", "smiling gnus are happy", "¥€$", "", "bleh" }) { + TestCase.assertEquals(x, CryptoUtil.deobfuscate(CryptoUtil.obfuscate(x), null)); + } + } +}