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