wownero
/
wownerujo
Archived
4
0
Fork 0

CrAzYpass implementation (#234)

upstream
m2049r 6 years ago committed by GitHub
parent 37f22a9dc2
commit 073bd96b17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

5
.gitignore vendored

@ -1,10 +1,7 @@
.gradle
/build
*.iml
/.idea/libraries
/.idea/workspace.xml
/.idea/caches
/.idea/codeStyles
/.idea
/local.properties
/captures
.externalNativeBuild

@ -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"

@ -695,6 +695,23 @@ Java_com_m2049r_xmrwallet_model_Wallet_isSynchronized(JNIEnv *env, jobject insta
return static_cast<jboolean>(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

@ -18,6 +18,7 @@
#define XMRWALLET_WALLET_LIB_H
#include <jni.h>
/*
#include <android/log.h>
@ -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 <typename T>
T *getHandle(JNIEnv *env, jobject obj, const char* fieldName = "handle") {
template<typename T>
T *getHandle(JNIEnv *env, jobject obj, const char *fieldName = "handle") {
jlong handle = env->GetLongField(obj, getHandleField(env, obj, fieldName));
return reinterpret_cast<T *>(handle);
}
@ -42,10 +43,27 @@ void setHandleFromLong(JNIEnv *env, jobject obj, jlong handle) {
env->SetLongField(obj, getHandleField(env, obj), handle);
}
template <typename T>
template<typename T>
void setHandle(JNIEnv *env, jobject obj, T *t) {
jlong handle = reinterpret_cast<jlong>(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

@ -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);
}
}
}

@ -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, Void, Boolean> {
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, Void, Boolean> {
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;
}
}

@ -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;

@ -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:

@ -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:

@ -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<std::string> &unknown_parameters, std::string &error) = 0;
//virtual bool rescanSpent() = 0;
}

@ -271,6 +271,7 @@ public class WalletManager {
//TODO static std::tuple<bool, std::string, std::string, std::string, std::string> 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);
}

@ -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
}
}
}

@ -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();

@ -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";
}
}

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/scrollview"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -7,7 +8,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ProgressBar
@ -18,55 +19,6 @@
android:indeterminate="true"
android:visibility="gone" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/header_top_first"
android:orientation="horizontal"
android:weightSum="2">
<TextView
style="@style/MoneroLabel.Heading"
android:layout_width="0sp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/generate_wallet_label"
android:textAlignment="center" />
<TextView
style="@style/MoneroLabel.Heading"
android:layout_width="0sp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/generate_password_label"
android:textAlignment="center" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/data_top"
android:orientation="horizontal"
android:weightSum="2">
<TextView
android:id="@+id/tvWalletName"
style="@style/MoneroText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAlignment="center" />
<TextView
android:id="@+id/tvWalletPassword"
style="@style/MoneroText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="***"
android:textAlignment="center" />
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -96,7 +48,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/data_top"
android:textAlignment="center" />
android:textAlignment="center"
tools:text="49RBjxQ2zgf7t17w7So9ngcEY9obKzsrr6Dsah24MNSMiMBEeiYPP5CCTBq4GpZcEYN5Zf3upsLiwd5PezePE1i4Tf3rryY" />
<FrameLayout
android:layout_width="match_parent"
@ -118,7 +71,33 @@
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/data_top"
android:background="@drawable/backgound_seed"
android:textAlignment="center" />
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" />
<LinearLayout
android:id="@+id/llPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/header_top"
android:orientation="vertical"
android:visibility="gone">
<TextView
style="@style/MoneroLabel.Heading"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:text="@string/generate_crazypass_label" />
<TextView
android:id="@+id/tvWalletPassword"
style="@style/MoneroText.Monospace"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/data_top"
android:textAlignment="center"
tools:text="2ExEN7droBLzWW2CEE2d7kv7E/iCZwNn:hKwUmg3:hg" />
</LinearLayout>
<Button
android:id="@+id/bAccept"
@ -178,20 +157,16 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/data_top"
android:textAlignment="center" />
android:textAlignment="center"
tools:text="e4aba454d78799dbd8d576bf70e7f15a06e91f1ecfd404053f91519a48df2a0e" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/header_top">
<TextView
style="@style/MoneroLabel.Heading"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:text="@string/generate_spendkey_label" />
</FrameLayout>
<TextView