Implmenet credential encryption

Encrypt server credentials using Android KeyStore rather than storing
them in plain text.

This fixes #33.
This commit is contained in:
rthakohov 2017-08-10 14:56:17 -07:00
parent cde6c2ae83
commit e0e5d1179e
5 changed files with 329 additions and 3 deletions

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View file

@ -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<String> {
private final Map<String, String> mServerStringMap = new HashMap<>();
private final CredentialCache mCredentialCache;
private EncryptionManager mEncryptionManager;
private final List<MountedShareChangeListener> 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<String> serverStringSet =
mPref.getStringSet(SERVER_STRING_SET_KEY, Collections.<String> emptySet());
final Map<String, ShareTuple> shareMap = new HashMap<>(serverStringSet.size());
final List<String> 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<String> {
mServerStringSet = new HashSet<>(serverStringSet);
encryptServers(forceEncryption);
for (Map.Entry<String, ShareTuple> server : shareMap.entrySet()) {
final ShareTuple tuple = server.getValue();
@ -133,8 +151,13 @@ public class ShareManager implements Iterable<String> {
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<String> {
}
}
private void encryptServers(List<String> 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;

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}