Merge pull request #32 from rthakohov/credential-encryption
Credential encryption
This commit is contained in:
commit
5b32f0dc18
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.JsonToken;
|
||||||
import android.util.JsonWriter;
|
import android.util.JsonWriter;
|
||||||
import android.util.Log;
|
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 com.google.android.sambadocumentsprovider.nativefacade.CredentialCache;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
|
@ -57,18 +60,31 @@ public class ShareManager implements Iterable<String> {
|
||||||
private final Map<String, String> mServerStringMap = new HashMap<>();
|
private final Map<String, String> mServerStringMap = new HashMap<>();
|
||||||
private final CredentialCache mCredentialCache;
|
private final CredentialCache mCredentialCache;
|
||||||
|
|
||||||
|
private EncryptionManager mEncryptionManager;
|
||||||
|
|
||||||
private final List<MountedShareChangeListener> mListeners = new ArrayList<>();
|
private final List<MountedShareChangeListener> mListeners = new ArrayList<>();
|
||||||
|
|
||||||
ShareManager(Context context, CredentialCache credentialCache) {
|
ShareManager(Context context, CredentialCache credentialCache) {
|
||||||
mCredentialCache = credentialCache;
|
mCredentialCache = credentialCache;
|
||||||
|
|
||||||
|
mEncryptionManager = new EncryptionManager(context);
|
||||||
|
|
||||||
mPref = context.getSharedPreferences(SERVER_CACHE_PREF_KEY, Context.MODE_PRIVATE);
|
mPref = context.getSharedPreferences(SERVER_CACHE_PREF_KEY, Context.MODE_PRIVATE);
|
||||||
// Loading saved servers.
|
// Loading saved servers.
|
||||||
final Set<String> serverStringSet =
|
final Set<String> serverStringSet =
|
||||||
mPref.getStringSet(SERVER_STRING_SET_KEY, Collections.<String> emptySet());
|
mPref.getStringSet(SERVER_STRING_SET_KEY, Collections.<String> emptySet());
|
||||||
|
|
||||||
final Map<String, ShareTuple> shareMap = new HashMap<>(serverStringSet.size());
|
final Map<String, ShareTuple> shareMap = new HashMap<>(serverStringSet.size());
|
||||||
|
final List<String> forceEncryption = new ArrayList<>();
|
||||||
for (String serverString : serverStringSet) {
|
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);
|
String uri = decode(serverString, shareMap);
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
mServerStringMap.put(uri, serverString);
|
mServerStringMap.put(uri, serverString);
|
||||||
|
@ -77,6 +93,8 @@ public class ShareManager implements Iterable<String> {
|
||||||
|
|
||||||
mServerStringSet = new HashSet<>(serverStringSet);
|
mServerStringSet = new HashSet<>(serverStringSet);
|
||||||
|
|
||||||
|
encryptServers(forceEncryption);
|
||||||
|
|
||||||
for (Map.Entry<String, ShareTuple> server : shareMap.entrySet()) {
|
for (Map.Entry<String, ShareTuple> server : shareMap.entrySet()) {
|
||||||
final ShareTuple tuple = server.getValue();
|
final ShareTuple tuple = server.getValue();
|
||||||
|
|
||||||
|
@ -133,8 +151,13 @@ public class ShareManager implements Iterable<String> {
|
||||||
if (serverString == null) {
|
if (serverString == null) {
|
||||||
throw new IllegalStateException("Failed to encode credential tuple.");
|
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) {
|
if (tuple.mIsMounted) {
|
||||||
mMountedServerSet.add(uri);
|
mMountedServerSet.add(uri);
|
||||||
} else {
|
} 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) {
|
public synchronized boolean unmountServer(String uri) {
|
||||||
if (!mServerStringMap.containsKey(uri)) {
|
if (!mServerStringMap.containsKey(uri)) {
|
||||||
return true;
|
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