Merge pull request #35 from syng-io/profile-encryption-changes

Profile encryption changes
This commit is contained in:
adrian-tiberius 2015-10-22 14:04:06 +03:00
commit 082cd9068e
9 changed files with 316 additions and 93 deletions

View File

@ -230,28 +230,30 @@ public abstract class BaseActivity extends AppCompatActivity implements
}
private void requestChangeProfile(final Profile profile) {
Dialog dialog = new MaterialDialog.Builder(BaseActivity.this)
.title(R.string.request_profile_password)
.customView(R.layout.profile_password, true)
.positiveText(R.string.ok)
.negativeText(R.string.cancel)
.callback(new MaterialDialog.ButtonCallback() {
@SuppressWarnings("ConstantConditions")
@Override
public void onPositive(MaterialDialog dialog) {
View view = dialog.getCustomView();
EditText password = (EditText) view.findViewById(R.id.et_pass);
if (profile.checkPassword(password.getText().toString())) {
ProfileManager.setCurrentProfile(profile);
flipDrawer();
} else {
Toast.makeText(BaseActivity.this, "Password is not correct", Toast.LENGTH_SHORT).show();
}
}
})
.show();
EditText name = (EditText) dialog.findViewById(R.id.et_pass);
GeneralUtil.showKeyBoard(name, this);
GeneralUtil.showProfilePasswordRequestDialog(BaseActivity.this, profile.getName(), new MaterialDialog.ButtonCallback() {
@SuppressWarnings("ConstantConditions")
@Override
public void onPositive(MaterialDialog dialog) {
View view = dialog.getCustomView();
EditText passwordText = (EditText) view.findViewById(R.id.et_pass);
String password = passwordText.getText().toString();
if (profile.checkPassword(password)) {
ProfileManager.setCurrentProfile(profile, password);
flipDrawer();
dialog.dismiss();
} else {
Toast.makeText(BaseActivity.this, "Password is not correct", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onNegative(MaterialDialog dialog) {
dialog.dismiss();
}
});
}
private void updateCurrentProfileName() {
@ -404,30 +406,27 @@ public abstract class BaseActivity extends AppCompatActivity implements
ProfileDialogFragment dialogFragment = ProfileDialogFragment.newInstance(profile);
dialogFragment.show(getSupportFragmentManager(), "profile_dialog");
} else {
Dialog dialog = new MaterialDialog.Builder(BaseActivity.this)
.title(R.string.request_profile_password)
.customView(R.layout.profile_password, true)
.positiveText(R.string.ok)
.negativeText(R.string.cancel)
.callback(new MaterialDialog.ButtonCallback() {
@SuppressWarnings("ConstantConditions")
@Override
public void onPositive(MaterialDialog dialog) {
View view = dialog.getCustomView();
EditText password = (EditText) view.findViewById(R.id.et_pass);
if (profile.checkPassword(password.getText().toString())) {
ProfileDialogFragment dialogFragment = ProfileDialogFragment.newInstance(profile);
dialogFragment.show(getSupportFragmentManager(), "profile_dialog");
} else {
Toast.makeText(BaseActivity.this, "Password is not correct", Toast.LENGTH_SHORT).show();
}
}
})
.show();
EditText name = (EditText) dialog.findViewById(R.id.et_pass);
GeneralUtil.showKeyBoard(name, this);
}
GeneralUtil.showProfilePasswordRequestDialog(BaseActivity.this, profile.getName(), new MaterialDialog.ButtonCallback() {
@Override
public void onPositive(MaterialDialog dialog) {
View view = dialog.getCustomView();
EditText password = (EditText) view.findViewById(R.id.et_pass);
if (profile.checkPassword(password.getText().toString())) {
dialog.dismiss();
ProfileDialogFragment dialogFragment = ProfileDialogFragment.newInstance(profile);
dialogFragment.show(getSupportFragmentManager(), "profile_dialog");
} else {
Toast.makeText(BaseActivity.this, "Password is not correct", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onNegative(MaterialDialog dialog) {
dialog.dismiss();
}
});
}
}
@Override

View File

@ -12,6 +12,7 @@ import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.Toast;
@ -44,9 +45,8 @@ public class LoginActivity extends AppCompatActivity {
if (PrefsUtil.isFirstLaunch()) {
createAndSetProfile();
} else {
startNextActivity();
loginWallet();
}
}
private void startNextActivity() {
@ -76,7 +76,31 @@ public class LoginActivity extends AppCompatActivity {
LoginActivity.this.finish();
dialog.dismiss();
}
});
}
private void loginWallet() {
final Profile profile = ProfileManager.getCurrentProfile();
GeneralUtil.showProfilePasswordRequestDialog(LoginActivity.this, profile.getName(), new MaterialDialog.ButtonCallback() {
@Override
public void onPositive(MaterialDialog dialog) {
View view = dialog.getCustomView();
EditText passwordText = (EditText) view.findViewById(R.id.et_pass);
String password = passwordText.getText().toString();
if (profile.checkPassword(password)) {
dialog.dismiss();
ProfileManager.setCurrentProfile(profile, password);
startNextActivity();
} else {
Toast.makeText(LoginActivity.this, "Password is not correct", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onNegative(MaterialDialog dialog) {
LoginActivity.this.finish();
dialog.dismiss();
}
});
}

View File

@ -33,6 +33,7 @@ import org.ethereum.net.p2p.HelloMessage;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.EnumSet;
import java.util.UUID;
@ -57,6 +58,8 @@ public class SyngApplication extends MultiDexApplication implements ConnectorHan
private boolean isRpcConnection = true;
public boolean isEthereumConnected = false;
private String mHandlerIdentifier = UUID.randomUUID().toString();
@Override
@ -92,8 +95,9 @@ public class SyngApplication extends MultiDexApplication implements ConnectorHan
@Override
public void onConnectorConnected() {
System.out.println("Connector connected");
isEthereumConnected = true;
SyngApplication.sEthereumConnector.addListener(mHandlerIdentifier, EnumSet.allOf(EventFlag.class));
sEthereumConnector.init(ProfileManager.getCurrentProfile().getPrivateKeys());
sEthereumConnector.init(new ArrayList<String>());
sEthereumConnector.startJsonRpc();
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
String rpcServer = sharedPref.getString(getString(R.string.pref_json_rpc_server_key), "http://rpc0.syng.io:8545/");
@ -103,6 +107,7 @@ public class SyngApplication extends MultiDexApplication implements ConnectorHan
@Override
public void onConnectorDisconnected() {
System.out.println("Connector Disconnected");
isEthereumConnected = false;
}
@Override

View File

@ -8,22 +8,24 @@
package io.syng.entity;
import android.util.Log;
import org.ethereum.crypto.ECKey;
import org.ethereum.crypto.HashUtil;
import org.ethereum.wallet.EtherSaleWallet;
import org.ethereum.wallet.EtherSaleWalletDecoder;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.util.encoders.Hex;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import io.syng.util.Encryption;
public class Profile implements Serializable {
private static final Logger logger = LoggerFactory.getLogger("Profile");
private static final String TAG = "Profile";
protected String name;
@ -31,6 +33,8 @@ public class Profile implements Serializable {
protected List<String> privateKeys = new ArrayList<>();
protected List<String> publicKeys = new ArrayList<>();
/* "password protect profile" (encrypt the private keys) */
protected boolean passwordProtectedProfile;
@ -42,18 +46,21 @@ public class Profile implements Serializable {
protected transient boolean isEncrypted;
public Profile() {
this.privateKeys.add(createPrivateKey());
addPrivateKey(createPrivateKey(), null);
addDefaultApps();
}
public Profile(String privateKey) {
this.privateKeys.add(privateKey);
addPrivateKey(privateKey, null);
addDefaultApps();
}
public Profile(List<String> privateKeys) {
this.privateKeys = privateKeys;
for (String privateKey: privateKeys) {
addPrivateKey(privateKey, null);
}
addDefaultApps();
}
@ -79,30 +86,53 @@ public class Profile implements Serializable {
return Hex.toHexString(privateKey);
}
public List<String> getPrivateKeys() {
return privateKeys;
public List<String> getPrivateKeys(String password) {
List<String> keys = new ArrayList<>();
for (String privateKey: privateKeys) {
String key = passwordProtectedProfile ? decryptPrivateKey(privateKey, password) : privateKey;
keys.add(key);
}
return keys;
}
public List<String> getAddresses() {
return publicKeys;
}
List<String> addresses = new ArrayList<>();
for (String privateKey : privateKeys) {
ECKey key = ECKey.fromPrivate(Hex.decode(privateKey));
addresses.add(Hex.toHexString(key.getAddress()));
public void addPrivateKeys(List<String> privateKeys, String password) {
for (String privateKey: privateKeys) {
addPrivateKey(privateKey, password);
}
return addresses;
}
public void setPrivateKeys(List<String> privateKeys) {
this.privateKeys = privateKeys;
protected String getPublicKey(String privateKey) {
ECKey ecKey = ECKey.fromPrivate(Hex.decode(privateKey));
return Hex.toHexString(ecKey.getAddress());
}
public void addPrivateKey(String privateKey) {
this.privateKeys.add(privateKey);
public boolean addPrivateKey(String privateKey, String password) {
if (password != null && passwordProtectedProfile) {
if (!checkPassword(password)) {
return false;
}
this.privateKeys.add(encryptPrivateKey(privateKey, password));
} else {
this.privateKeys.add(privateKey);
}
this.publicKeys.add(getPublicKey(privateKey));
return true;
}
private String hash(String text) {
return Hex.toHexString(HashUtil.sha3(text.getBytes()));
}
public void removePrivateKey(String privateKey) {
this.privateKeys.remove(privateKey);
this.publicKeys.remove(getPublicKey(privateKey));
}
public List<Dapp> getDapps() {
@ -155,20 +185,23 @@ public class Profile implements Serializable {
}
public void setPassword(String password) {
this.passwordHash = Hex.toHexString(HashUtil.sha3(password.getBytes()));
if (!passwordProtectedProfile) {
this.passwordHash = hash(password);
this.encrypt(password);
}
}
public boolean checkPassword(String password) {
return passwordHash.equals(Hex.toHexString(HashUtil.sha3(password.getBytes())));
return passwordHash.equals(hash(password));
}
public void encrypt(String password) {
if (!passwordProtectedProfile) {
setPassword(password);
List<String> encrypted = new ArrayList<>();
for (String privateKey : this.privateKeys) {
encrypted.add(encryptPrivateKey(privateKey, password));
}
this.privateKeys.clear();
this.privateKeys = encrypted;
passwordProtectedProfile = true;
}
@ -176,7 +209,7 @@ public class Profile implements Serializable {
public boolean decrypt(String password) {
if (passwordProtectedProfile) {
if (!passwordHash.equals(Hex.toHexString(HashUtil.sha3(password.getBytes())))) {
if (!checkPassword(password)) {
return false;
}
List<String> decrypted = new ArrayList<>();
@ -190,16 +223,22 @@ public class Profile implements Serializable {
}
protected String encryptPrivateKey(String privateKey, String password) {
// TODO: Encrypt private key
return privateKey;
String encryptedKey = Encryption.encrypt(privateKey, password);
if (encryptedKey == null) {
Log.w(TAG, "Could not encrypt private key");
}
return encryptedKey;
}
protected String decryptPrivateKey(String privateKey, String password) {
// TODO: Decrypt private key
return privateKey;
String decryptedKey = Encryption.decrypt(privateKey, password);
if (decryptedKey == null) {
Log.w(TAG, "Could not decrypt private key");
}
return decryptedKey;
}
public boolean importWallet(String jsonWallet, String password) {
public boolean importWallet(String jsonWallet, String importPassword, String currentPassword) {
try {
JSONObject json = new JSONObject(jsonWallet);
byte[] privateKey = null;
@ -210,35 +249,51 @@ public class Profile implements Serializable {
wallet.setEmail(json.getString("email"));
wallet.setBtcaddr(json.getString("btcaddr"));
EtherSaleWalletDecoder decoder = new EtherSaleWalletDecoder(wallet);
privateKey = decoder.getPrivateKey(password);
privateKey = decoder.getPrivateKey(importPassword);
} else if (json.has("Crypto")) {
wallet.setEncseed(json.getJSONObject("Crypto").getJSONObject("cipherparams").getString("iv") + json.getJSONObject("Crypto").getString("ciphertext"));
wallet.setEthaddr(json.getString("address"));
EtherSaleWalletDecoder decoder = new EtherSaleWalletDecoder(wallet);
privateKey = decoder.getPrivateKey(password);
privateKey = decoder.getPrivateKey(importPassword);
}
if (privateKey == null) {
logger.warn("Invalid json wallet file.");
Log.w(TAG, "Invalid json wallet file.");
return false;
}
ECKey key = ECKey.fromPrivate(privateKey);
String address = Hex.toHexString(key.getAddress());
if (address.equals(wallet.getEthaddr())) {
privateKeys.add(Hex.toHexString(privateKey));
String keyToAdd = Hex.toHexString(privateKey);
if (passwordProtectedProfile) {
addPrivateKey(keyToAdd, currentPassword);
} else {
addPrivateKey(keyToAdd, null);
}
this.publicKeys.add(getPublicKey(keyToAdd));
} else {
logger.warn("Invalid wallet password.");
Log.w(TAG, "Invalid wallet password.");
return false;
}
} catch (Exception e) {
logger.error("Error importing wallet.", e);
Log.e(TAG, "Error importing wallet: " + e.getMessage());
return false;
}
return true;
}
public void importPrivateKey(String privateKey, String password) {
privateKeys.add(decryptPrivateKey(privateKey, password));
public boolean importPrivateKey(String privateKey, String importedKeyPassword, String walletPassword) {
String decryptedKey = importedKeyPassword == null || importedKeyPassword == "" ? privateKey : decryptPrivateKey(privateKey, importedKeyPassword);
if (passwordProtectedProfile) {
if (checkPassword(walletPassword)) {
addPrivateKey(decryptedKey, walletPassword);
} else {
return false;
}
} else {
addPrivateKey(decryptedKey, null);
}
return true;
}
@Override

View File

@ -51,10 +51,6 @@ public class ProfileKeysFragment extends Fragment {
View view = inflater.inflate(R.layout.fragment_profile_keys, container, false);
Profile profile = ProfileManager.getProfileById(mProfileId);
if (profile != null) {
profile.getPrivateKeys();
}
mRecyclerView = (RecyclerView) view.findViewById(R.id.rv_profile_keys);
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getActivity());
mRecyclerView.setLayoutManager(layoutManager);

View File

@ -0,0 +1,107 @@
/*
* Copyright (c) 2015 Jarrad Hope
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package io.syng.util;
import android.util.Base64;
import android.util.Log;
import java.security.SecureRandom;
import java.security.spec.KeySpec;
import java.util.regex.Pattern;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
public class Encryption {
private static final String TAG = "Encryption";
private static SecureRandom random = new SecureRandom();
private static String delimiter = "}";
private static int keyLength = 256;
private static int saltLength = keyLength / 8;
private static int iterationCount = 20000;
public static String encrypt(String text, String password) {
Log.d(TAG, "Encrypting: " + text);
String encryptedText = "";
byte[] salt = generateSalt(saltLength);
SecretKey key = generateKey(password, salt);
if (key != null) {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
byte[] iv = new byte[cipher.getBlockSize()];
random.nextBytes(iv);
IvParameterSpec ivParams = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, key, ivParams);
byte[] encryptedKey = cipher.doFinal(text.getBytes("UTF-8"));
encryptedText = Base64.encodeToString(salt, Base64.NO_WRAP);
encryptedText += delimiter + Base64.encodeToString(iv, Base64.NO_WRAP);
encryptedText += delimiter + Base64.encodeToString(encryptedKey, Base64.NO_WRAP);
Log.d(TAG, "Encrypted: " + encryptedText);
return encryptedText;
} catch (Exception e) {
Log.e(TAG, "encrypt(): " + e.toString());
}
}
return null;
}
private static byte[] generateSalt(int saltLength) {
byte[] salt = new byte[saltLength];
random.nextBytes(salt);
return salt;
}
private static SecretKey generateKey(String password, byte[] salt) {
try {
KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, iterationCount, keyLength);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
byte[] keyBytes = keyFactory.generateSecret(keySpec).getEncoded();
return new SecretKeySpec(keyBytes, "AES");
} catch (Exception e) {
Log.e(TAG, "generateKey(): " + e.toString());
}
return null;
}
public static String decrypt(String encryptedText, String password) {
Log.d(TAG, "Decrypting: " + encryptedText);
String[] parts = encryptedText.split(Pattern.quote(delimiter));
byte[] salt = Base64.decode(parts[0], Base64.NO_WRAP);
byte[] iv = Base64.decode(parts[1], Base64.NO_WRAP);
byte[] cipherBytes = Base64.decode(parts[2], Base64.NO_WRAP);
SecretKey key = generateKey(password, salt);
if (key != null) {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec ivParams = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, key, ivParams);
byte[] decrypted = cipher.doFinal(cipherBytes);
String decryptedText = new String(decrypted, "UTF-8");
Log.d(TAG, "Decrypted: " + decryptedText);
return decryptedText;
} catch (Exception e) {
Log.e(TAG, "decrypt(): " + e.toString());
}
}
return null;
}
}

View File

@ -112,7 +112,7 @@ public final class GeneralUtil {
profile.setName(name.getText().toString());
profile.setPassword(pass1String);
ProfileManager.addProfile(profile);
ProfileManager.setCurrentProfile(profile);
ProfileManager.setCurrentProfile(profile, pass1String);
GeneralUtil.hideKeyBoard(name, context);
GeneralUtil.hideKeyBoard(pass1, context);
GeneralUtil.hideKeyBoard(pass2, context);
@ -150,6 +150,33 @@ public final class GeneralUtil {
GeneralUtil.showKeyBoard(name, context);
}
public static void showProfilePasswordRequestDialog(final Context context, String profileName, MaterialDialog.ButtonCallback callback) {
if (callback == null) {
callback = new MaterialDialog.ButtonCallback() {
@Override
public void onPositive(MaterialDialog dialog) {
dialog.dismiss();
}
@Override
public void onNegative(MaterialDialog dialog) {
dialog.dismiss();
}
};
}
MaterialDialog dialog = new MaterialDialog.Builder(context)
.title((profileName != null ? profileName + " " : "") + context.getResources().getString(R.string.request_profile_password))
.positiveText(R.string.ok)
.negativeText(R.string.cancel)
.customView(R.layout.profile_password, true)
.callback(callback)
.autoDismiss(false)
.show();
EditText name = (EditText) dialog.findViewById(R.id.et_pass);
GeneralUtil.showKeyBoard(name, context);
}
public static void showDAppEditDialog(final Dapp dapp, final Context context) {
MaterialDialog dialog = new MaterialDialog.Builder(context)
.title("Edit")
@ -256,8 +283,10 @@ public final class GeneralUtil {
RadioButton importJsonRadio = (RadioButton) dialog.findViewById(R.id.radio_import_json);
EditText importPathEdit = (EditText) dialog.findViewById(R.id.wallet_import_path);
EditText walletPasswordEdit = (EditText) dialog.findViewById(R.id.wallet_password);
EditText currentWalletPasswordEdit = (EditText) dialog.findViewById(R.id.current_wallet_password);
String importPath = importPathEdit.getText().toString();
String password = walletPasswordEdit.getText().toString();
String currentPassword = currentWalletPasswordEdit.getText().toString();
String fileContents = null;
try {
File walletFile = new File(importPath);
@ -281,15 +310,15 @@ public final class GeneralUtil {
}
if (importJsonRadio.isChecked()) {
Profile profile = ProfileManager.getCurrentProfile();
if (profile.importWallet(fileContents, password)) {
if (profile.importWallet(fileContents, password, currentPassword)) {
ProfileManager.updateProfile(profile);
ProfileManager.setCurrentProfile(profile);
ProfileManager.setCurrentProfile(profile, currentPassword);
} else {
Toast.makeText(context, R.string.invalid_wallet_password, Toast.LENGTH_SHORT).show();
}
} else {
Profile profile = ProfileManager.getCurrentProfile();
profile.importPrivateKey(fileContents, password);
profile.importPrivateKey(fileContents, password, currentPassword);
ProfileManager.updateProfile(profile);
}
}

View File

@ -110,8 +110,8 @@ public final class ProfileManager {
return new Profile();
}
public static void setCurrentProfile(Profile profile) {
List<String> privateKeys = profile.getPrivateKeys();
public static void setCurrentProfile(Profile profile, String password) {
List<String> privateKeys = profile.getPrivateKeys(password);
SyngApplication.sEthereumConnector.init(privateKeys);
PrefsUtil.setCurrentProfileId(profile.getId());
notifyListener();

View File

@ -38,7 +38,15 @@
android:id="@+id/wallet_password"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:hint="Password"
android:hint="Import Password"
android:singleLine="true"
android:textColor="@color/accent"/>
<EditText
android:id="@+id/current_wallet_password"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:hint="Current Wallet Password"
android:singleLine="true"
android:textColor="@color/accent"/>
</LinearLayout>