Implmenet credential encryption
Encrypt server credentials using Android KeyStore rather than storing them in plain text. This fixes #33.
This commit is contained in:
parent
cde6c2ae83
commit
e0e5d1179e
5 changed files with 329 additions and 3 deletions
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue