diff --git a/.gitignore b/.gitignore index 661833e5..002a926a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,7 @@ .gradle /build *.iml -/.idea/libraries -/.idea/workspace.xml -/.idea/caches -/.idea/codeStyles +/.idea /local.properties /captures .externalNativeBuild diff --git a/app/build.gradle b/app/build.gradle index 61a6797b..0aa2c8f9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.m2049r.xmrwallet" minSdkVersion 21 targetSdkVersion 25 - versionCode 87 - versionName "1.4.7 'Monero Spedner'" + versionCode 90 + versionName "1.5.0 'CrAzYpass'" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" externalNativeBuild { cmake { @@ -66,7 +66,6 @@ dependencies { implementation 'com.android.support:support-v4:25.4.0' implementation 'com.android.support:recyclerview-v7:25.4.0' implementation 'com.android.support:cardview-v7:25.4.0' - implementation 'com.android.support.constraint:constraint-layout:1.0.2' implementation 'me.dm7.barcodescanner:zxing:1.9.8' implementation "com.squareup.okhttp3:okhttp:$rootProject.ext.okHttpVersion" diff --git a/app/src/main/cpp/monerujo.cpp b/app/src/main/cpp/monerujo.cpp index 01f23425..a7adbb82 100644 --- a/app/src/main/cpp/monerujo.cpp +++ b/app/src/main/cpp/monerujo.cpp @@ -695,6 +695,23 @@ Java_com_m2049r_xmrwallet_model_Wallet_isSynchronized(JNIEnv *env, jobject insta return static_cast(wallet->synchronized()); } +//void cn_slow_hash(const void *data, size_t length, char *hash); // from crypto/hash-ops.h +JNIEXPORT jbyteArray JNICALL +Java_com_m2049r_xmrwallet_util_KeyStoreHelper_cnSlowHash(JNIEnv *env, jobject clazz, + jbyteArray data) { + + jbyte *buffer = env->GetByteArrayElements(data, NULL); + jsize size = env->GetArrayLength(data); + char hash[HASH_SIZE]; + cn_slow_hash(buffer, (size_t) size, hash); + + env->ReleaseByteArrayElements(data, buffer, JNI_ABORT); // do not update java byte[] + + jbyteArray result = env->NewByteArray(HASH_SIZE); + env->SetByteArrayRegion(result, 0, HASH_SIZE, (jbyte *) hash); + return result; +} + JNIEXPORT jstring JNICALL Java_com_m2049r_xmrwallet_model_Wallet_getDisplayAmount(JNIEnv *env, jobject clazz, jlong amount) { @@ -1083,7 +1100,8 @@ Java_com_m2049r_xmrwallet_model_PendingTransaction_getTxCount(JNIEnv *env, jobje //static void error(const std::string &category, const std::string &str); JNIEXPORT void JNICALL Java_com_m2049r_xmrwallet_model_WalletManager_initLogger(JNIEnv *env, jobject instance, - jstring argv0, jstring default_log_base_name) { + jstring argv0, + jstring default_log_base_name) { const char *_argv0 = env->GetStringUTFChars(argv0, NULL); const char *_default_log_base_name = env->GetStringUTFChars(default_log_base_name, NULL); @@ -1109,7 +1127,7 @@ Java_com_m2049r_xmrwallet_model_WalletManager_logDebug(JNIEnv *env, jobject inst JNIEXPORT void JNICALL Java_com_m2049r_xmrwallet_model_WalletManager_logInfo(JNIEnv *env, jobject instance, - jstring category, jstring message) { + jstring category, jstring message) { const char *_category = env->GetStringUTFChars(category, NULL); const char *_message = env->GetStringUTFChars(message, NULL); @@ -1122,7 +1140,7 @@ Java_com_m2049r_xmrwallet_model_WalletManager_logInfo(JNIEnv *env, jobject insta JNIEXPORT void JNICALL Java_com_m2049r_xmrwallet_model_WalletManager_logWarning(JNIEnv *env, jobject instance, - jstring category, jstring message) { + jstring category, jstring message) { const char *_category = env->GetStringUTFChars(category, NULL); const char *_message = env->GetStringUTFChars(message, NULL); @@ -1149,11 +1167,10 @@ Java_com_m2049r_xmrwallet_model_WalletManager_logError(JNIEnv *env, jobject inst JNIEXPORT void JNICALL Java_com_m2049r_xmrwallet_model_WalletManager_setLogLevel(JNIEnv *env, jobject instance, jint level) { - Bitmonero::WalletManagerFactory::setLogLevel(level); + Bitmonero::WalletManagerFactory::setLogLevel(level); } - #ifdef __cplusplus } #endif diff --git a/app/src/main/cpp/monerujo.h b/app/src/main/cpp/monerujo.h index 3315e022..de74f221 100644 --- a/app/src/main/cpp/monerujo.h +++ b/app/src/main/cpp/monerujo.h @@ -18,6 +18,7 @@ #define XMRWALLET_WALLET_LIB_H #include + /* #include @@ -27,13 +28,13 @@ #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__) */ -jfieldID getHandleField(JNIEnv *env, jobject obj, const char* fieldName = "handle") { +jfieldID getHandleField(JNIEnv *env, jobject obj, const char *fieldName = "handle") { jclass c = env->GetObjectClass(obj); return env->GetFieldID(c, fieldName, "J"); // of type long } -template -T *getHandle(JNIEnv *env, jobject obj, const char* fieldName = "handle") { +template +T *getHandle(JNIEnv *env, jobject obj, const char *fieldName = "handle") { jlong handle = env->GetLongField(obj, getHandleField(env, obj, fieldName)); return reinterpret_cast(handle); } @@ -42,10 +43,27 @@ void setHandleFromLong(JNIEnv *env, jobject obj, jlong handle) { env->SetLongField(obj, getHandleField(env, obj), handle); } -template +template void setHandle(JNIEnv *env, jobject obj, T *t) { jlong handle = reinterpret_cast(t); setHandleFromLong(env, obj, handle); } +#ifdef __cplusplus +extern "C" +{ +#endif + +// from monero-core crypto/hash-ops.h - avoid #including monero code here +enum { + HASH_SIZE = 32, + HASH_DATA_AREA = 136 +}; + +void cn_slow_hash(const void *data, size_t length, char *hash); + +#ifdef __cplusplus +} +#endif + #endif //XMRWALLET_WALLET_LIB_H diff --git a/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java b/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java index c2b59562..a1726074 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java @@ -34,6 +34,7 @@ import android.view.inputmethod.EditorInfo; import android.widget.Button; import android.widget.TextView; +import com.m2049r.xmrwallet.util.KeyStoreHelper; import com.m2049r.xmrwallet.util.RestoreHeight; import com.m2049r.xmrwallet.widget.Toolbar; import com.m2049r.xmrwallet.model.Wallet; @@ -424,17 +425,20 @@ public class GenerateFragment extends Fragment { String name = etWalletName.getEditText().getText().toString(); String password = etWalletPassword.getEditText().getText().toString(); + // create the real wallet password + String crazyPass = KeyStoreHelper.getCrazyPass(getActivity(), password); + long height = getHeight(); if (height < 0) height = 0; if (type.equals(TYPE_NEW)) { bGenerate.setEnabled(false); - activityCallback.onGenerate(name, password); + activityCallback.onGenerate(name, crazyPass); } else if (type.equals(TYPE_SEED)) { if (!checkMnemonic()) return; String seed = etWalletMnemonic.getEditText().getText().toString(); bGenerate.setEnabled(false); - activityCallback.onGenerate(name, password, seed, height); + activityCallback.onGenerate(name, crazyPass, seed, height); } else if (type.equals(TYPE_KEY) || type.equals(TYPE_VIEWONLY)) { if (checkAddress() && checkViewKey() && checkSpendKey()) { bGenerate.setEnabled(false); @@ -444,7 +448,7 @@ public class GenerateFragment extends Fragment { if (type.equals(TYPE_KEY)) { spendKey = etWalletSpendKey.getEditText().getText().toString(); } - activityCallback.onGenerate(name, password, address, viewKey, spendKey, height); + activityCallback.onGenerate(name, crazyPass, address, viewKey, spendKey, height); } } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java b/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java index 97a4b330..b99a271b 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java @@ -16,16 +16,23 @@ package com.m2049r.xmrwallet; +import android.app.AlertDialog; import android.content.Context; +import android.content.DialogInterface; import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.Nullable; +import android.support.design.widget.TextInputLayout; import android.support.v4.app.Fragment; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; import android.widget.Button; import android.widget.ImageButton; import android.widget.LinearLayout; @@ -35,12 +42,15 @@ import android.widget.TextView; import android.widget.Toast; import com.m2049r.xmrwallet.model.NetworkType; +import com.m2049r.xmrwallet.util.KeyStoreHelper; import com.m2049r.xmrwallet.widget.Toolbar; import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.model.WalletManager; import com.m2049r.xmrwallet.util.Helper; import com.m2049r.xmrwallet.util.MoneroThreadPoolExecutor; +import java.io.File; + import timber.log.Timber; public class GenerateReviewFragment extends Fragment { @@ -51,7 +61,6 @@ public class GenerateReviewFragment extends Fragment { ScrollView scrollview; ProgressBar pbProgress; - TextView tvWalletName; TextView tvWalletPassword; TextView tvWalletAddress; TextView tvWalletMnemonic; @@ -59,9 +68,18 @@ public class GenerateReviewFragment extends Fragment { TextView tvWalletSpendKey; ImageButton bCopyAddress; LinearLayout llAdvancedInfo; + LinearLayout llPassword; Button bAdvancedInfo; Button bAccept; + // TODO fix visibility of variables + String walletPath; + String walletName; + // we need to keep the password so the user is not asked again if they want to change it + // note they can only enter this fragment immediately after entering the password + // so asking them to enter it a couple of seconds later seems silly + String walletPassword = null; + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -70,7 +88,6 @@ public class GenerateReviewFragment extends Fragment { scrollview = (ScrollView) view.findViewById(R.id.scrollview); pbProgress = (ProgressBar) view.findViewById(R.id.pbProgress); - tvWalletName = (TextView) view.findViewById(R.id.tvWalletName); tvWalletPassword = (TextView) view.findViewById(R.id.tvWalletPassword); tvWalletAddress = (TextView) view.findViewById(R.id.tvWalletAddress); tvWalletViewKey = (TextView) view.findViewById(R.id.tvWalletViewKey); @@ -79,12 +96,14 @@ public class GenerateReviewFragment extends Fragment { bCopyAddress = (ImageButton) view.findViewById(R.id.bCopyAddress); bAdvancedInfo = (Button) view.findViewById(R.id.bAdvancedInfo); llAdvancedInfo = (LinearLayout) view.findViewById(R.id.llAdvancedInfo); + llPassword = (LinearLayout) view.findViewById(R.id.llPassword); bAccept = (Button) view.findViewById(R.id.bAccept); boolean testnet = WalletManager.getInstance().getNetworkType() != NetworkType.NetworkType_Mainnet; tvWalletMnemonic.setTextIsSelectable(testnet); tvWalletSpendKey.setTextIsSelectable(testnet); + tvWalletPassword.setTextIsSelectable(testnet); bAccept.setOnClickListener(new View.OnClickListener() { @Override @@ -112,17 +131,20 @@ public class GenerateReviewFragment extends Fragment { } }); - showProgress(); - Bundle args = getArguments(); - String path = args.getString("path"); - String password = args.getString("password"); - this.type = args.getString("type"); - new AsyncShow().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR, - path, password); + type = args.getString("type"); + walletPath = args.getString("path"); + showDetails(args.getString("password")); return view; } + void showDetails(String password) { + walletPassword = password; + showProgress(); + tvWalletPassword.setText(null); + new AsyncShow().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR, walletPath); + } + void copyViewKey() { Helper.clipBoardCopy(getActivity(), getString(R.string.label_copy_viewkey), tvWalletViewKey.getText().toString()); Toast.makeText(getActivity(), getString(R.string.message_copy_viewkey), Toast.LENGTH_SHORT).show(); @@ -151,15 +173,11 @@ public class GenerateReviewFragment extends Fragment { String type; private void acceptWallet() { - String name = tvWalletName.getText().toString(); - String password = tvWalletPassword.getText().toString(); bAccept.setEnabled(false); - acceptCallback.onAccept(name, password); + acceptCallback.onAccept(walletName, walletPassword); } private class AsyncShow extends AsyncTask { - String password; - String name; String address; String seed; @@ -170,9 +188,8 @@ public class GenerateReviewFragment extends Fragment { @Override protected Boolean doInBackground(String... params) { - if (params.length != 2) return false; + if (params.length != 1) return false; String walletPath = params[0]; - password = params[1]; Wallet wallet; boolean closeWallet; @@ -180,7 +197,7 @@ public class GenerateReviewFragment extends Fragment { wallet = GenerateReviewFragment.this.walletCallback.getWallet(); closeWallet = false; } else { - wallet = WalletManager.getInstance().openWallet(walletPath, password); + wallet = WalletManager.getInstance().openWallet(walletPath, walletPassword); closeWallet = true; } name = wallet.getName(); @@ -204,13 +221,16 @@ public class GenerateReviewFragment extends Fragment { protected void onPostExecute(Boolean result) { super.onPostExecute(result); if (!isAdded()) return; // never mind - tvWalletName.setText(name); + walletName = name; if (result) { if (type.equals(GenerateReviewFragment.VIEW_TYPE_ACCEPT)) { - tvWalletPassword.setText(password); bAccept.setVisibility(View.VISIBLE); bAccept.setEnabled(true); } + if (walletPassword != null) { + llPassword.setVisibility(View.VISIBLE); + tvWalletPassword.setText(walletPassword); + } tvWalletAddress.setText(address); tvWalletMnemonic.setText(seed); tvWalletViewKey.setText(viewKey); @@ -233,6 +253,7 @@ public class GenerateReviewFragment extends Fragment { } Listener activityCallback = null; + ProgressListener progressCallback = null; AcceptListener acceptCallback = null; ListenerWithWallet walletCallback = null; @@ -242,6 +263,13 @@ public class GenerateReviewFragment extends Fragment { void setToolbarButton(int type); } + public interface ProgressListener { + void showProgressDialog(int msgId); + + void dismissProgressDialog(); + } + + public interface AcceptListener { void onAccept(String name, String password); } @@ -256,6 +284,9 @@ public class GenerateReviewFragment extends Fragment { if (context instanceof Listener) { this.activityCallback = (Listener) context; } + if (context instanceof ProgressListener) { + this.progressCallback = (ProgressListener) context; + } if (context instanceof AcceptListener) { this.acceptCallback = (AcceptListener) context; } @@ -268,9 +299,7 @@ public class GenerateReviewFragment extends Fragment { public void onResume() { super.onResume(); Timber.d("onResume()"); - String name = tvWalletName.getText().toString(); - if (name.isEmpty()) name = null; - activityCallback.setTitle(name, getString(R.string.details_title)); + activityCallback.setTitle(walletName, getString(R.string.details_title)); activityCallback.setToolbarButton( GenerateReviewFragment.VIEW_TYPE_ACCEPT.equals(type) ? Toolbar.BUTTON_NONE : Toolbar.BUTTON_BACK); } @@ -295,7 +324,198 @@ public class GenerateReviewFragment extends Fragment { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.wallet_details_menu, menu); - super.onCreateOptionsMenu(menu, inflater); + String type = getArguments().getString("type"); + if (GenerateReviewFragment.VIEW_TYPE_ACCEPT.equals(type)) { + inflater.inflate(R.menu.wallet_details_help_menu, menu); + super.onCreateOptionsMenu(menu, inflater); + } else { + inflater.inflate(R.menu.wallet_details_menu, menu); + super.onCreateOptionsMenu(menu, inflater); + } } + + boolean changeWalletPassword(String newPassword) { + Wallet wallet; + boolean closeWallet; + if (type.equals(GenerateReviewFragment.VIEW_TYPE_WALLET)) { + wallet = GenerateReviewFragment.this.walletCallback.getWallet(); + closeWallet = false; + } else { + wallet = WalletManager.getInstance().openWallet(walletPath, walletPassword); + closeWallet = true; + } + + boolean ok = false; + if (wallet.getStatus() == Wallet.Status.Status_Ok) { + wallet.setPassword(newPassword); + wallet.store(); + ok = true; + } else { + Timber.e(wallet.getErrorString()); + } + if (closeWallet) wallet.close(); + return ok; + } + + private class AsyncChangePassword extends AsyncTask { + String newPassword; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + if (progressCallback != null) + progressCallback.showProgressDialog(R.string.changepw_progress); + } + + @Override + protected Boolean doInBackground(String... params) { + if (params.length != 3) return false; + File walletFile = Helper.getWalletFile(getActivity(), params[0]); + String oldPassword = params[1]; + String userPassword = params[2]; + newPassword = KeyStoreHelper.getCrazyPass(getActivity(), userPassword); + return changeWalletPassword(newPassword); + } + + @Override + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + if (getActivity().isDestroyed()) { + return; + } + if (progressCallback != null) + progressCallback.dismissProgressDialog(); + if (result) { + Toast.makeText(getActivity(), getString(R.string.changepw_success), Toast.LENGTH_SHORT).show(); + showDetails(newPassword); + } else { + Toast.makeText(getActivity(), getString(R.string.changepw_failed), Toast.LENGTH_LONG).show(); + } + } + } + + AlertDialog openDialog = null; // for preventing opening of multiple dialogs + + public AlertDialog createChangePasswordDialog() { + if (openDialog != null) return null; // we are already open + LayoutInflater li = LayoutInflater.from(getActivity()); + View promptsView = li.inflate(R.layout.prompt_changepw, null); + + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity()); + alertDialogBuilder.setView(promptsView); + + final TextInputLayout etPasswordA = (TextInputLayout) promptsView.findViewById(R.id.etWalletPasswordA); + etPasswordA.setHint(getString(R.string.prompt_changepw, walletName)); + + final TextInputLayout etPasswordB = (TextInputLayout) promptsView.findViewById(R.id.etWalletPasswordB); + etPasswordB.setHint(getString(R.string.prompt_changepwB, walletName)); + + etPasswordA.getEditText().addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable s) { + if (etPasswordA.getError() != null) { + etPasswordA.setError(null); + } + if (etPasswordB.getError() != null) { + etPasswordB.setError(null); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, + int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, + int before, int count) { + } + }); + + etPasswordB.getEditText().addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable s) { + if (etPasswordA.getError() != null) { + etPasswordA.setError(null); + } + if (etPasswordB.getError() != null) { + etPasswordB.setError(null); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, + int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, + int before, int count) { + } + }); + + // set dialog message + alertDialogBuilder + .setCancelable(false) + .setPositiveButton(getString(R.string.label_ok), null) + .setNegativeButton(getString(R.string.label_cancel), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + Helper.hideKeyboardAlways(getActivity()); + dialog.cancel(); + openDialog = null; + } + }); + + openDialog = alertDialogBuilder.create(); + openDialog.setOnShowListener(new DialogInterface.OnShowListener() { + @Override + public void onShow(final DialogInterface dialog) { + Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE); + button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + String newPasswordA = etPasswordA.getEditText().getText().toString(); + String newPasswordB = etPasswordB.getEditText().getText().toString(); + // disallow empty passwords + if (newPasswordA.isEmpty()) { + etPasswordA.setError(getString(R.string.generate_empty_passwordB)); + } else if (!newPasswordA.equals(newPasswordB)) { + etPasswordB.setError(getString(R.string.generate_bad_passwordB)); + } else if (newPasswordA.equals(newPasswordB)) { + new AsyncChangePassword().execute(walletName, walletPassword, newPasswordA); + Helper.hideKeyboardAlways(getActivity()); + openDialog.dismiss(); + openDialog = null; + } + } + }); + } + }); + + // accept keyboard "ok" + etPasswordB.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() { + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) { + String newPasswordA = etPasswordA.getEditText().getText().toString(); + String newPasswordB = etPasswordB.getEditText().getText().toString(); + // disallow empty passwords + if (newPasswordA.isEmpty()) { + etPasswordA.setError(getString(R.string.generate_empty_passwordB)); + } else if (!newPasswordA.equals(newPasswordB)) { + etPasswordB.setError(getString(R.string.generate_bad_passwordB)); + } else if (newPasswordA.equals(newPasswordB)) { + new AsyncChangePassword().execute(walletName, walletPassword, newPasswordA); + Helper.hideKeyboardAlways(getActivity()); + openDialog.dismiss(); + openDialog = null; + } + return true; + } + return false; + } + }); + return openDialog; + } + } diff --git a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java index 271d084f..3e458d1a 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java @@ -54,7 +54,9 @@ import com.m2049r.xmrwallet.model.NetworkType; import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.model.WalletManager; import com.m2049r.xmrwallet.service.WalletService; +import com.m2049r.xmrwallet.util.CrazyPassEncoder; import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.KeyStoreHelper; import com.m2049r.xmrwallet.util.MoneroThreadPoolExecutor; import com.m2049r.xmrwallet.widget.Toolbar; @@ -71,7 +73,8 @@ import timber.log.Timber; public class LoginActivity extends SecureActivity implements LoginFragment.Listener, GenerateFragment.Listener, - GenerateReviewFragment.Listener, GenerateReviewFragment.AcceptListener, ReceiveFragment.Listener { + GenerateReviewFragment.Listener, GenerateReviewFragment.AcceptListener, + GenerateReviewFragment.ProgressListener, ReceiveFragment.Listener { private static final String GENERATE_STACK = "gen"; static final int DAEMON_TIMEOUT = 500; // deamon must respond in 500ms @@ -437,16 +440,30 @@ public class LoginActivity extends SecureActivity } } + public void onWalletChangePassword() {//final String walletName, final String walletPassword) { + try { + GenerateReviewFragment detailsFragment = (GenerateReviewFragment) + getSupportFragmentManager().findFragmentById(R.id.fragment_container); + AlertDialog dialog = detailsFragment.createChangePasswordDialog(); + if (dialog != null) { + Helper.showKeyboard(dialog); + dialog.show(); + } + } catch (ClassCastException ex) { + Timber.w("onWalletChangePassword() called, but no GenerateReviewFragment active"); + } + } + @Override public void onAddWallet(String type) { if (checkServiceRunning()) return; startGenerateFragment(type); } - AlertDialog passwordDialog = null; // for preventing multiple clicks in wallet list + AlertDialog openDialog = null; // for preventing opening of multiple dialogs void promptPassword(final String wallet, final PasswordAction action) { - if (passwordDialog != null) return; // we are already asking for password + if (openDialog != null) return; // we are already asking for password Context context = LoginActivity.this; LayoutInflater li = LayoutInflater.from(context); View promptsView = li.inflate(R.layout.prompt_password, null); @@ -486,12 +503,12 @@ public class LoginActivity extends SecureActivity public void onClick(DialogInterface dialog, int id) { Helper.hideKeyboardAlways(LoginActivity.this); dialog.cancel(); - passwordDialog = null; + openDialog = null; } }); - passwordDialog = alertDialogBuilder.create(); + openDialog = alertDialogBuilder.create(); - passwordDialog.setOnShowListener(new DialogInterface.OnShowListener() { + openDialog.setOnShowListener(new DialogInterface.OnShowListener() { @Override public void onShow(DialogInterface dialog) { Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE); @@ -501,8 +518,8 @@ public class LoginActivity extends SecureActivity String pass = etPassword.getEditText().getText().toString(); if (processPasswordEntry(wallet, pass, action)) { Helper.hideKeyboardAlways(LoginActivity.this); - passwordDialog.dismiss(); - passwordDialog = null; + openDialog.dismiss(); + openDialog = null; } else { etPassword.setError(getString(R.string.bad_password)); } @@ -511,8 +528,6 @@ public class LoginActivity extends SecureActivity } }); - Helper.showKeyboard(passwordDialog); - // accept keyboard "ok" etPassword.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() { public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { @@ -520,8 +535,8 @@ public class LoginActivity extends SecureActivity String pass = etPassword.getEditText().getText().toString(); if (processPasswordEntry(wallet, pass, action)) { Helper.hideKeyboardAlways(LoginActivity.this); - passwordDialog.dismiss(); - passwordDialog = null; + openDialog.dismiss(); + openDialog = null; } else { etPassword.setError(getString(R.string.bad_password)); } @@ -531,14 +546,37 @@ public class LoginActivity extends SecureActivity } }); - passwordDialog.show(); + Helper.showKeyboard(openDialog); + openDialog.show(); } - private boolean checkWalletPassword(String walletName, String password) { + // try to figure out what the real wallet password is given the user password + // which could be the actual wallet password or a (maybe malformed) CrAzYpass + // or the password used to derive the CrAzYpass for the wallet + private String getWalletPassword(String walletName, String password) { String walletPath = new File(Helper.getWalletRoot(getApplicationContext()), walletName + ".keys").getAbsolutePath(); - // only test view key - return WalletManager.getInstance().verifyWalletPassword(walletPath, password, true); + + // try with entered password (which could be a legacy password or a CrAzYpass) + if (WalletManager.getInstance().verifyWalletPassword(walletPath, password, true)) { + return password; + } + + // maybe this is a malformed CrAzYpass? + String possibleCrazyPass = CrazyPassEncoder.reformat(password); + if (possibleCrazyPass != null) { // looks like a CrAzYpass + if (WalletManager.getInstance().verifyWalletPassword(walletPath, possibleCrazyPass, true)) { + return possibleCrazyPass; + } + } + + // generate & try with CrAzYpass + String crazyPass = KeyStoreHelper.getCrazyPass(this, password); + if (WalletManager.getInstance().verifyWalletPassword(walletPath, crazyPass, true)) { + return crazyPass; + } + + return null; } interface PasswordAction { @@ -546,8 +584,9 @@ public class LoginActivity extends SecureActivity } private boolean processPasswordEntry(String walletName, String pass, PasswordAction action) { - if (checkWalletPassword(walletName, pass)) { - action.action(walletName, pass); + String walletPassword = getWalletPassword(walletName, pass); + if (walletPassword != null) { + action.action(walletName, walletPassword); return true; } else { return false; @@ -598,7 +637,8 @@ public class LoginActivity extends SecureActivity ProgressDialog progressDialog = null; - private void showProgressDialog(int msgId) { + @Override + public void showProgressDialog(int msgId) { showProgressDialog(msgId, 0); } @@ -617,7 +657,8 @@ public class LoginActivity extends SecureActivity } } - private void dismissProgressDialog() { + @Override + public void dismissProgressDialog() { if (progressDialog != null && progressDialog.isShowing()) { progressDialog.dismiss(); } @@ -1077,6 +1118,9 @@ public class LoginActivity extends SecureActivity case R.id.action_details_help: HelpFragment.display(getSupportFragmentManager(), R.string.help_details); return true; + case R.id.action_details_changepw: + onWalletChangePassword(); + return true; case R.id.action_license_info: AboutFragment.display(getSupportFragmentManager()); return true; diff --git a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java index 0108baa0..87284258 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java @@ -174,6 +174,9 @@ public class WalletActivity extends SecureActivity implements WalletFragment.Lis case R.id.action_details_help: HelpFragment.display(getSupportFragmentManager(), R.string.help_details); return true; + case R.id.action_details_changepw: + onWalletChangePassword(); + return true; case R.id.action_help_send: HelpFragment.display(getSupportFragmentManager(), R.string.help_send); return true; @@ -182,6 +185,20 @@ public class WalletActivity extends SecureActivity implements WalletFragment.Lis } } + public void onWalletChangePassword() {//final String walletName, final String walletPassword) { + try { + GenerateReviewFragment detailsFragment = (GenerateReviewFragment) + getSupportFragmentManager().findFragmentById(R.id.fragment_container); + AlertDialog dialog = detailsFragment.createChangePasswordDialog(); + if (dialog != null) { + Helper.showKeyboard(dialog); + dialog.show(); + } + } catch (ClassCastException ex) { + Timber.w("onWalletChangePassword() called, but no GenerateReviewFragment active"); + } + } + @Override protected void onCreate(Bundle savedInstanceState) { Timber.d("onCreate()"); @@ -682,6 +699,7 @@ public class WalletActivity extends SecureActivity implements WalletFragment.Lis case DialogInterface.BUTTON_POSITIVE: Bundle extras = new Bundle(); extras.putString("type", GenerateReviewFragment.VIEW_TYPE_WALLET); + extras.putString("password", getIntent().getExtras().getString(REQUEST_PW)); replaceFragment(new GenerateReviewFragment(), null, extras); break; case DialogInterface.BUTTON_NEGATIVE: diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendFragment.java index c5e61e24..bdafc043 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendFragment.java @@ -291,7 +291,7 @@ public class SendFragment extends Fragment pagerAdapter.notifyDataSetChanged(); } }); - Timber.d("New Mode = " + mode.toString()); + Timber.d("New Mode = %s", mode.toString()); } } @@ -350,7 +350,7 @@ public class SendFragment extends Fragment @Override public SendWizardFragment getItem(int position) { Timber.d("getItem(%d) CREATE", position); - Timber.d("Mode=" + mode.toString()); + Timber.d("Mode=%s", mode.toString()); if (mode == Mode.XMR) { switch (position) { case POS_ADDRESS: diff --git a/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java b/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java index 76ab0c4c..7c205561 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java +++ b/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java @@ -275,5 +275,4 @@ public class Wallet { //virtual bool parse_uri(const std::string &uri, std::string &address, std::string &payment_id, uint64_t &tvAmount, std::string &tx_description, std::string &recipient_name, std::vector &unknown_parameters, std::string &error) = 0; //virtual bool rescanSpent() = 0; - } diff --git a/app/src/main/java/com/m2049r/xmrwallet/model/WalletManager.java b/app/src/main/java/com/m2049r/xmrwallet/model/WalletManager.java index 47b2ed18..5bd6a68e 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/model/WalletManager.java +++ b/app/src/main/java/com/m2049r/xmrwallet/model/WalletManager.java @@ -271,6 +271,7 @@ public class WalletManager { //TODO static std::tuple checkUpdates(const std::string &software, const std::string &subdir); static public native void initLogger(String argv0, String defaultLogBaseName); + //TODO: maybe put these in an enum like in monero core - but why? static public int LOGLEVEL_SILENT = -1; static public int LOGLEVEL_WARN = 0; @@ -278,9 +279,14 @@ public class WalletManager { static public int LOGLEVEL_DEBUG = 2; static public int LOGLEVEL_TRACE = 3; static public int LOGLEVEL_MAX = 4; + static public native void setLogLevel(int level); + static public native void logDebug(String category, String message); + static public native void logInfo(String category, String message); + static public native void logWarning(String category, String message); + static public native void logError(String category, String message); } \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/CrazyPassEncoder.java b/app/src/main/java/com/m2049r/xmrwallet/util/CrazyPassEncoder.java new file mode 100644 index 00000000..cd884d71 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/CrazyPassEncoder.java @@ -0,0 +1,54 @@ +package com.m2049r.xmrwallet.util; + +import com.m2049r.xmrwallet.model.WalletManager; + +import java.math.BigInteger; + +public class CrazyPassEncoder { + static final String BASE = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"; + static final int PW_CHARS = 52; + + // this takes a 32 byte buffer and converts it to 52 alphnumeric characters + // separated by blanks every 4 characters = 13 groups of 4 + // always (padding by Xs if need be + static public String encode(byte[] data) { + if (data.length != 32) throw new IllegalArgumentException("data[] is not 32 bytes long"); + BigInteger rest = new BigInteger(1, data); + BigInteger remainder; + final StringBuilder result = new StringBuilder(); + final BigInteger base = BigInteger.valueOf(BASE.length()); + int i = 0; + do { + if ((i > 0) && (i % 4 == 0)) result.append(' '); + i++; + remainder = rest.remainder(base); + rest = rest.divide(base); + result.append(BASE.charAt(remainder.intValue())); + } while (!BigInteger.ZERO.equals(rest)); + // pad it + while (i < PW_CHARS) { + if ((i > 0) && (i % 4 == 0)) result.append(' '); + result.append('2'); + i++; + } + return result.toString(); + } + + static public String reformat(String password) { + // maybe this is a CrAzYpass without blanks? or lowercase letters + String noBlanks = password.toUpperCase().replaceAll(" ", ""); + if (noBlanks.length() == PW_CHARS) { // looks like a CrAzYpass + // insert blanks every 4 characters + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < PW_CHARS; i++) { + if ((i > 0) && (i % 4 == 0)) sb.append(' '); + char c = noBlanks.charAt(i); + if (BASE.indexOf(c) < 0) return null; // invalid character found + sb.append(c); + } + return sb.toString(); + } else { + return null; // not a CrAzYpass + } + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java b/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java index db5d8233..7bc83572 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java +++ b/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java @@ -47,6 +47,7 @@ import com.m2049r.xmrwallet.model.WalletManager; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; +import java.math.BigInteger; import java.net.MalformedURLException; import java.net.SocketTimeoutException; import java.net.URL; @@ -280,6 +281,14 @@ public class Helper { } } + private final static char[] HexArray = "0123456789ABCDEF".toCharArray(); + + public static String bytesToHex(byte[] data) { + if ((data != null) && (data.length > 0)) + return String.format("%0" + (data.length * 2) + "X", new BigInteger(1, data)); + else return ""; + } + static public void setMoneroHome(Context context) { try { String home = getStorage(context, HOME_DIR).getAbsolutePath(); diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/KeyStoreHelper.java b/app/src/main/java/com/m2049r/xmrwallet/util/KeyStoreHelper.java new file mode 100644 index 00000000..ea49cd54 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/KeyStoreHelper.java @@ -0,0 +1,217 @@ +/* + * Copyright 2018 m2049r + * Copyright 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.util; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.security.KeyPairGeneratorSpec; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.SignatureException; +import java.util.Calendar; +import java.util.GregorianCalendar; + +import javax.security.auth.x500.X500Principal; + +import timber.log.Timber; + +public class KeyStoreHelper { + + static { + System.loadLibrary("monerujo"); + } + + public static native byte[] cnSlowHash(byte[] data); + + static final private String RSA_ALIAS = "MonerujoRSA"; + + public static String getCrazyPass(Context context, String password) { + // TODO we should check Locale.getDefault().getLanguage() here but for now we default to English + return getCrazyPass(context, password, "English"); + } + + public static String getCrazyPass(Context context, String password, String language) { + byte[] data = password.getBytes(StandardCharsets.UTF_8); + byte[] sig = null; + try { + KeyStoreHelper.createKeys(context, RSA_ALIAS); + sig = KeyStoreHelper.signData(RSA_ALIAS, data); + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + return CrazyPassEncoder.encode(cnSlowHash(sig)); + } + + /** + * Creates a public and private key and stores it using the Android Key + * Store, so that only this application will be able to access the keys. + */ + public static void createKeys(Context context, String alias) throws NoSuchProviderException, + NoSuchAlgorithmException, InvalidAlgorithmParameterException, KeyStoreException { + KeyStore keyStore = KeyStore.getInstance(SecurityConstants.KEYSTORE_PROVIDER_ANDROID_KEYSTORE); + try { + keyStore.load(null); + } catch (Exception ex) { // don't care why it failed + throw new IllegalStateException("Could not load KeySotre", ex); + } + if (!keyStore.containsAlias(alias)) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + createKeysJBMR2(context, alias); + } else { + createKeysM(alias); + } + } + } + + public static void deleteKeys(String alias) throws KeyStoreException { + KeyStore keyStore = KeyStore.getInstance(SecurityConstants.KEYSTORE_PROVIDER_ANDROID_KEYSTORE); + try { + keyStore.load(null); + keyStore.deleteEntry(alias); + } catch (Exception ex) { // don't care why it failed + throw new IllegalStateException("Could not load KeySotre", ex); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + private static void createKeysJBMR2(Context context, String alias) throws NoSuchProviderException, + NoSuchAlgorithmException, InvalidAlgorithmParameterException { + + Calendar start = new GregorianCalendar(); + Calendar end = new GregorianCalendar(); + end.add(Calendar.YEAR, 300); + + KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(context) + .setAlias(alias) + .setSubject(new X500Principal("CN=" + alias)) + .setSerialNumber(BigInteger.valueOf(Math.abs(alias.hashCode()))) + .setStartDate(start.getTime()).setEndDate(end.getTime()) + .build(); + // defaults to 2048 bit modulus + KeyPairGenerator kpGenerator = KeyPairGenerator.getInstance( + SecurityConstants.TYPE_RSA, + SecurityConstants.KEYSTORE_PROVIDER_ANDROID_KEYSTORE); + kpGenerator.initialize(spec); + KeyPair kp = kpGenerator.generateKeyPair(); + Timber.d("preM Keys created"); + } + + @TargetApi(Build.VERSION_CODES.M) + private static void createKeysM(String alias) { + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_RSA, SecurityConstants.KEYSTORE_PROVIDER_ANDROID_KEYSTORE); + keyPairGenerator.initialize( + new KeyGenParameterSpec.Builder( + alias, KeyProperties.PURPOSE_SIGN) + .setDigests(KeyProperties.DIGEST_SHA256) + .setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1) + .build()); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + Timber.d("M Keys created"); + } catch (NoSuchProviderException | NoSuchAlgorithmException | InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); + } + } + + private static KeyStore.PrivateKeyEntry getPrivateKeyEntry(String alias) { + try { + KeyStore ks = KeyStore + .getInstance(SecurityConstants.KEYSTORE_PROVIDER_ANDROID_KEYSTORE); + ks.load(null); + KeyStore.Entry entry = ks.getEntry(alias, null); + + if (entry == null) { + Timber.w("No key found under alias: %s", alias); + return null; + } + + if (!(entry instanceof KeyStore.PrivateKeyEntry)) { + Timber.w("Not an instance of a PrivateKeyEntry"); + return null; + } + return (KeyStore.PrivateKeyEntry) entry; + } catch (Exception ex) { + Timber.e(ex); + return null; + } + } + + /** + * Signs the data using the key pair stored in the Android Key Store. This + * signature can be used with the data later to verify it was signed by this + * application. + * + * @return The data signature generated + */ + public static byte[] signData(String alias, byte[] data) throws NoSuchAlgorithmException, + InvalidKeyException, SignatureException { + + PrivateKey privateKey = getPrivateKeyEntry(alias).getPrivateKey(); + Signature s = Signature.getInstance(SecurityConstants.SIGNATURE_SHA256withRSA); + s.initSign(privateKey); + s.update(data); + return s.sign(); + } + + /** + * Given some data and a signature, uses the key pair stored in the Android + * Key Store to verify that the data was signed by this application, using + * that key pair. + * + * @param data The data to be verified. + * @param signature The signature provided for the data. + * @return A boolean value telling you whether the signature is valid or + * not. + */ + public static boolean verifyData(String alias, byte[] data, byte[] signature) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + + // Make sure the signature string exists + if (signature == null) { + Timber.w("Invalid signature."); + return false; + } + + KeyStore.PrivateKeyEntry keyEntry = getPrivateKeyEntry(alias); + Signature s = Signature.getInstance(SecurityConstants.SIGNATURE_SHA256withRSA); + s.initVerify(keyEntry.getCertificate()); + s.update(data); + return s.verify(signature); + } + + public interface SecurityConstants { + String KEYSTORE_PROVIDER_ANDROID_KEYSTORE = "AndroidKeyStore"; + String TYPE_RSA = "RSA"; + String SIGNATURE_SHA256withRSA = "SHA256withRSA"; + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_review.xml b/app/src/main/res/layout/fragment_review.xml index fb847763..569a665b 100644 --- a/app/src/main/res/layout/fragment_review.xml +++ b/app/src/main/res/layout/fragment_review.xml @@ -1,5 +1,6 @@ - - - - - - - - - - - - - - + android:textAlignment="center" + tools:text="49RBjxQ2zgf7t17w7So9ngcEY9obKzsrr6Dsah24MNSMiMBEeiYPP5CCTBq4GpZcEYN5Zf3upsLiwd5PezePE1i4Tf3rryY" /> + android:textAlignment="center" + tools:text="tucks slackens vehicle doctor oaks aloof balding knife rays wise haggled cuisine navy ladder suitcase dusted last thorn pixels karate ticket nibs violin zapped slackens" /> + + + + + + +