diff --git a/app/src/androidTest/java/com/google/android/sambadocumentsprovider/encryption/EncryptionManagerTests.java b/app/src/androidTest/java/com/google/android/sambadocumentsprovider/encryption/EncryptionManagerTests.java
new file mode 100644
index 0000000..222f65f
--- /dev/null
+++ b/app/src/androidTest/java/com/google/android/sambadocumentsprovider/encryption/EncryptionManagerTests.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.google.android.sambadocumentsprovider.encryption;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class EncryptionManagerTests {
+ private static final String TEST_STRING = "testtring";
+
+ private EncryptionManager mManager;
+
+ @Rule
+ public ExpectedException mThrown = ExpectedException.none();
+
+ @Before
+ public void init() throws EncryptionException {
+ mManager = new EncryptionManager(InstrumentationRegistry.getTargetContext());
+ }
+
+ @Test
+ public void encryption_encryptAndDecryptAreConsistent() throws EncryptionException {
+ String encrypted = mManager.encrypt(TEST_STRING);
+
+ assertEquals(TEST_STRING, mManager.decrypt(encrypted));
+ }
+
+ @Test
+ public void encryption_decryptOnPlainDataThrows() throws EncryptionException {
+ mThrown.expect(EncryptionException.class);
+
+ mManager.decrypt(TEST_STRING);
+ }
+}
diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/ShareManager.java b/app/src/main/java/com/google/android/sambadocumentsprovider/ShareManager.java
index 34e0e54..0549973 100644
--- a/app/src/main/java/com/google/android/sambadocumentsprovider/ShareManager.java
+++ b/app/src/main/java/com/google/android/sambadocumentsprovider/ShareManager.java
@@ -24,6 +24,9 @@ import android.util.JsonReader;
import android.util.JsonToken;
import android.util.JsonWriter;
import android.util.Log;
+
+import com.google.android.sambadocumentsprovider.encryption.EncryptionException;
+import com.google.android.sambadocumentsprovider.encryption.EncryptionManager;
import com.google.android.sambadocumentsprovider.nativefacade.CredentialCache;
import java.io.IOException;
import java.io.StringReader;
@@ -57,18 +60,31 @@ public class ShareManager implements Iterable {
private final Map mServerStringMap = new HashMap<>();
private final CredentialCache mCredentialCache;
+ private EncryptionManager mEncryptionManager;
+
private final List mListeners = new ArrayList<>();
ShareManager(Context context, CredentialCache credentialCache) {
mCredentialCache = credentialCache;
+ mEncryptionManager = new EncryptionManager(context);
+
mPref = context.getSharedPreferences(SERVER_CACHE_PREF_KEY, Context.MODE_PRIVATE);
// Loading saved servers.
final Set serverStringSet =
mPref.getStringSet(SERVER_STRING_SET_KEY, Collections. emptySet());
+
final Map shareMap = new HashMap<>(serverStringSet.size());
+ final List forceEncryption = new ArrayList<>();
for (String serverString : serverStringSet) {
- // TODO: Add decryption
+ try {
+ serverString = mEncryptionManager.decrypt(serverString);
+ } catch (EncryptionException e) {
+ Log.i(TAG, "Failed to decrypt server data: ", e);
+
+ forceEncryption.add(serverString);
+ }
+
String uri = decode(serverString, shareMap);
if (uri != null) {
mServerStringMap.put(uri, serverString);
@@ -77,6 +93,8 @@ public class ShareManager implements Iterable {
mServerStringSet = new HashSet<>(serverStringSet);
+ encryptServers(forceEncryption);
+
for (Map.Entry server : shareMap.entrySet()) {
final ShareTuple tuple = server.getValue();
@@ -133,8 +151,13 @@ public class ShareManager implements Iterable {
if (serverString == null) {
throw new IllegalStateException("Failed to encode credential tuple.");
}
- // TODO: Add encryption
- mServerStringSet.add(serverString);
+
+ try {
+ mServerStringSet.add(mEncryptionManager.encrypt(serverString));
+ } catch (EncryptionException e) {
+ throw new IllegalStateException("Failed to encrypt server data", e);
+ }
+
if (tuple.mIsMounted) {
mMountedServerSet.add(uri);
} else {
@@ -169,6 +192,18 @@ public class ShareManager implements Iterable {
}
}
+ private void encryptServers(List servers) {
+ for (String server : servers) {
+ try {
+ mServerStringSet.add(mEncryptionManager.encrypt(server));
+ } catch (EncryptionException e) {
+ Log.e(TAG, "Failed to encrypt server data: ", e);
+ }
+ }
+
+ mPref.edit().putStringSet(SERVER_STRING_SET_KEY, mServerStringSet).apply();
+ }
+
public synchronized boolean unmountServer(String uri) {
if (!mServerStringMap.containsKey(uri)) {
return true;
diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/encryption/EncryptionException.java b/app/src/main/java/com/google/android/sambadocumentsprovider/encryption/EncryptionException.java
new file mode 100644
index 0000000..1ac288e
--- /dev/null
+++ b/app/src/main/java/com/google/android/sambadocumentsprovider/encryption/EncryptionException.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.google.android.sambadocumentsprovider.encryption;
+
+public class EncryptionException extends Exception {
+ public EncryptionException(String message) {
+ super("Encryption failed: " + message);
+ }
+
+ public EncryptionException(String message, Throwable cause) {
+ super("Encryption failed: " + message, cause);
+ }
+}
diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/encryption/EncryptionKey.java b/app/src/main/java/com/google/android/sambadocumentsprovider/encryption/EncryptionKey.java
new file mode 100644
index 0000000..2ebf0a4
--- /dev/null
+++ b/app/src/main/java/com/google/android/sambadocumentsprovider/encryption/EncryptionKey.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.google.android.sambadocumentsprovider.encryption;
+
+import javax.crypto.SecretKey;
+
+class EncryptionKey {
+ private final SecretKey mKey;
+ private final byte[] mIv;
+
+ EncryptionKey(SecretKey key, byte[] iv) {
+ mKey = key;
+ mIv = iv;
+ }
+
+ SecretKey getKey() {
+ return mKey;
+ }
+
+ byte[] getIv() {
+ return mIv;
+ }
+}
diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/encryption/EncryptionManager.java b/app/src/main/java/com/google/android/sambadocumentsprovider/encryption/EncryptionManager.java
new file mode 100644
index 0000000..d6f9941
--- /dev/null
+++ b/app/src/main/java/com/google/android/sambadocumentsprovider/encryption/EncryptionManager.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.google.android.sambadocumentsprovider.encryption;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyProperties;
+import android.util.Base64;
+import android.util.Log;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.Random;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+
+public class EncryptionManager {
+ private static final String TAG = "EncryptionManager";
+
+ private static final String ENCRYPTION_MANAGER_PREF_KEY = "encryptionManager";
+ private static final String KEYSTORE_PROVIDER = "AndroidKeyStore";
+ private static final String KEY_ALIAS = "SambaEncryptionKey";
+ private static final String AES_CIPHER = KeyProperties.KEY_ALGORITHM_AES + "/" +
+ KeyProperties.BLOCK_MODE_GCM + "/" +
+ KeyProperties.ENCRYPTION_PADDING_NONE;
+ private static final int GCM_TAG_LENGTH = 128;
+ private static final int IV_LENGTH = 12;
+ private static final String DEFAULT_CHARSET = "UTF-8";
+
+ private static final Random RANDOM = new Random();
+
+ private final SharedPreferences mPref;
+ private final EncryptionKey mKey;
+ private final KeyStore mStore;
+
+ public EncryptionManager(Context context) {
+ mPref = context.getSharedPreferences(ENCRYPTION_MANAGER_PREF_KEY, Context.MODE_PRIVATE);
+
+ mStore = loadKeyStore();
+ mKey = loadKey();
+ }
+
+ public String encrypt(String data) throws EncryptionException {
+ try {
+ Cipher cipher = Cipher.getInstance(AES_CIPHER);
+ cipher.init(
+ Cipher.ENCRYPT_MODE, mKey.getKey(),
+ new GCMParameterSpec(GCM_TAG_LENGTH, mKey.getIv()));
+
+ byte[] encrypted = cipher.doFinal(data.getBytes(Charset.forName(DEFAULT_CHARSET)));
+
+ return Base64.encodeToString(encrypted, Base64.DEFAULT);
+ } catch (Exception e) {
+ throw new EncryptionException("Failed to encrypt data: ", e);
+ }
+ }
+
+ public String decrypt(String data) throws EncryptionException {
+ try {
+ Cipher cipher = Cipher.getInstance(AES_CIPHER);
+ cipher.init(
+ Cipher.DECRYPT_MODE, mKey.getKey(),
+ new GCMParameterSpec(GCM_TAG_LENGTH, mKey.getIv()));
+
+ byte[] decrypted = cipher.doFinal(Base64.decode(data, Base64.DEFAULT));
+ return new String(decrypted, Charset.forName(DEFAULT_CHARSET));
+ } catch (Exception e) {
+ throw new EncryptionException("Failed to decrypt data: ", e);
+ }
+ }
+
+ private EncryptionKey loadKey() {
+ SecretKey key;
+ KeyGenerator keyGen;
+
+ try {
+ key = (SecretKey) mStore.getKey(KEY_ALIAS, null);
+ if (key != null) {
+ return new EncryptionKey(key, loadIv());
+ }
+
+ keyGen = KeyGenerator.getInstance(
+ KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER);
+
+ KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(
+ KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
+ .setRandomizedEncryptionRequired(false)
+ .build();
+ keyGen.init(spec);
+ } catch (GeneralSecurityException e) {
+ // Should never happen.
+ throw new RuntimeException("Failed to load encryption key: ", e);
+ }
+
+ key = keyGen.generateKey();
+ byte[] iv = generateIv();
+
+ saveIv(iv);
+
+ return new EncryptionKey(key, iv);
+ }
+
+ private KeyStore loadKeyStore() {
+ try {
+ KeyStore store = KeyStore.getInstance(KEYSTORE_PROVIDER);
+ store.load(null);
+ return store;
+ } catch (GeneralSecurityException | IOException e) {
+ // Should never happen.
+ throw new RuntimeException("Falied to init EncryptionManager: ", e);
+ }
+ }
+
+ private static byte[] generateIv() {
+ byte[] iv = new byte[IV_LENGTH];
+
+ RANDOM.nextBytes(iv);
+
+ return iv;
+ }
+
+ private byte[] loadIv() {
+ String data = mPref.getString(KEY_ALIAS, null);
+ if (data == null) {
+ byte[] iv = generateIv();
+ saveIv(iv);
+ return iv;
+ }
+
+ return Base64.decode(data, Base64.DEFAULT);
+ }
+
+ private void saveIv(byte[] iv) {
+ String data = Base64.encodeToString(iv, Base64.DEFAULT);
+ mPref.edit().putString(KEY_ALIAS, data).apply();
+ }
+}