Basic onboarding flow, allows for wallet passwords.

NOTE: This commit still logs seeds to files for dev purposes, as there is no UI for it yet.
This commit is contained in:
pokkst 2022-09-07 23:04:28 -05:00
parent 6674f894b7
commit 528bc7c7c5
No known key found for this signature in database
GPG key ID: 90C2ED85E67A50FF
16 changed files with 384 additions and 297 deletions

View file

@ -1,20 +1,30 @@
package com.m2049r.xmrwallet;
import android.os.Bundle;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.navigation.fragment.NavHostFragment;
import com.m2049r.xmrwallet.fragment.dialog.PasswordBottomSheetDialog;
import com.m2049r.xmrwallet.fragment.dialog.SendBottomSheetDialog;
import com.m2049r.xmrwallet.livedata.SingleLiveEvent;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.service.AddressService;
import com.m2049r.xmrwallet.service.BalanceService;
import com.m2049r.xmrwallet.service.HistoryService;
import com.m2049r.xmrwallet.service.MoneroHandlerThread;
import com.m2049r.xmrwallet.service.PrefService;
import com.m2049r.xmrwallet.service.TxService;
import com.m2049r.xmrwallet.util.Constants;
import java.io.File;
public class MainActivity extends AppCompatActivity implements MoneroHandlerThread.Listener {
public class MainActivity extends AppCompatActivity implements MoneroHandlerThread.Listener, PasswordBottomSheetDialog.PasswordListener {
public final SingleLiveEvent restartEvents = new SingleLiveEvent();
private MoneroHandlerThread thread = null;
private TxService txService = null;
private BalanceService balanceService = null;
@ -25,21 +35,39 @@ public class MainActivity extends AppCompatActivity implements MoneroHandlerThre
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
File walletFile = new File(getApplicationInfo().dataDir, Constants.WALLET_NAME);
new PrefService(this);
if(walletFile.exists()) {
boolean promptPassword = PrefService.getInstance().getBoolean(Constants.PREF_USES_PASSWORD, false);
if(!promptPassword) {
init(walletFile, "");
} else {
PasswordBottomSheetDialog passwordDialog = new PasswordBottomSheetDialog();
passwordDialog.listener = this;
passwordDialog.show(getSupportFragmentManager(), null);
}
} else {
navigate(R.id.onboarding_fragment);
}
}
private void navigate(int destination) {
FragmentActivity activity = this;
FragmentManager fm = activity.getSupportFragmentManager();
NavHostFragment navHostFragment =
(NavHostFragment) fm.findFragmentById(R.id.nav_host_fragment);
if (navHostFragment != null) {
navHostFragment.getNavController().navigate(destination);
}
}
public MoneroHandlerThread getThread() {
return thread;
}
private void init() {
File walletFile = new File(getApplicationInfo().dataDir, "xmr_wallet");
Wallet wallet = null;
if (walletFile.exists()) {
wallet = WalletManager.getInstance().openWallet(walletFile.getAbsolutePath(), "");
} else {
wallet = WalletManager.getInstance().createWallet(walletFile, "", "English", 0);
}
public void init(File walletFile, String password) {
Wallet wallet = WalletManager.getInstance().openWallet(walletFile.getAbsolutePath(), password);
WalletManager.getInstance().setProxy("127.0.0.1:9050");
thread = new MoneroHandlerThread("WalletService", wallet, this);
this.txService = new TxService(this, thread);
@ -55,4 +83,16 @@ public class MainActivity extends AppCompatActivity implements MoneroHandlerThre
this.balanceService.refreshBalance();
this.addressService.refreshAddress();
}
@Override
public void onPasswordSuccess(String password) {
File walletFile = new File(getApplicationInfo().dataDir, Constants.WALLET_NAME);
init(walletFile, password);
restartEvents.call();
}
@Override
public void onPasswordFail() {
Toast.makeText(this, R.string.bad_password, Toast.LENGTH_SHORT).show();
}
}

View file

@ -0,0 +1,73 @@
package com.m2049r.xmrwallet.fragment.dialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.service.BalanceService;
import com.m2049r.xmrwallet.service.TxService;
import com.m2049r.xmrwallet.util.Constants;
import com.m2049r.xmrwallet.util.Helper;
import java.io.File;
public class PasswordBottomSheetDialog extends BottomSheetDialogFragment {
public PasswordListener listener = null;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.password_bottom_sheet_dialog, null);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
File walletFile = new File(getActivity().getApplicationInfo().dataDir, Constants.WALLET_NAME);
ImageButton pastePasswordImageButton = view.findViewById(R.id.paste_password_imagebutton);
EditText passwordEditText = view.findViewById(R.id.wallet_password_edittext);
Button unlockWalletButton = view.findViewById(R.id.unlock_wallet_button);
pastePasswordImageButton.setOnClickListener(view1 -> {
passwordEditText.setText(Helper.getClipBoardText(view.getContext()));
});
unlockWalletButton.setOnClickListener(view1 -> {
String password = passwordEditText.getText().toString();
boolean success = checkPassword(walletFile, password);
if(success) {
listener.onPasswordSuccess(password);
dismiss();
} else {
listener.onPasswordFail();
}
});
}
private boolean checkPassword(File walletFile, String password) {
Wallet wallet = WalletManager.getInstance().openWallet(walletFile.getAbsolutePath(), password);
boolean ok = wallet.getStatus().isOk();
wallet.close();
return ok;
}
public interface PasswordListener {
void onPasswordSuccess(String password);
void onPasswordFail();
}
}

View file

@ -1,12 +1,12 @@
package com.m2049r.xmrwallet.fragment.dialog;
import android.content.ClipboardManager;
import android.os.Bundle;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.service.BalanceService;
import com.m2049r.xmrwallet.service.TxService;
import com.m2049r.xmrwallet.util.Helper;
import android.view.LayoutInflater;
import android.view.View;
@ -21,7 +21,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
public class SendBottomSheetDialog extends BottomSheetDialogFragment {
private MutableLiveData<Boolean> _sendingMax = new MutableLiveData<>(false);
@ -42,12 +41,8 @@ public class SendBottomSheetDialog extends BottomSheetDialogFragment {
Button sendButton = view.findViewById(R.id.send_button);
TextView sendAllTextView = view.findViewById(R.id.sending_all_textview);
TxService.getInstance().clearSendEvent.observe(getViewLifecycleOwner(), o -> {
dismiss();
});
pasteAddressImageButton.setOnClickListener(view1 -> {
addressEditText.setText(Helper.getClipBoardText(view.getContext()));
});
sendMaxButton.setOnClickListener(view1 -> {
@ -68,7 +63,12 @@ public class SendBottomSheetDialog extends BottomSheetDialogFragment {
return;
}
sendButton.setEnabled(false);
TxService.getInstance().sendTx(address, amount, sendAll);
boolean success = TxService.getInstance().sendTx(address, amount, sendAll);
if(success) {
dismiss();
} else {
Toast.makeText(getActivity(), getString(R.string.error_sending_tx), Toast.LENGTH_SHORT).show();
}
} else if (!validAddress) {
Toast.makeText(getActivity(), getString(R.string.send_address_invalid), Toast.LENGTH_SHORT).show();
} else {

View file

@ -16,6 +16,7 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
@ -32,7 +33,9 @@ import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.service.AddressService;
import com.m2049r.xmrwallet.service.BalanceService;
import com.m2049r.xmrwallet.service.HistoryService;
import com.m2049r.xmrwallet.service.PrefService;
import com.m2049r.xmrwallet.service.TxService;
import com.m2049r.xmrwallet.util.Constants;
import java.util.Collections;
@ -49,9 +52,19 @@ public class HomeFragment extends Fragment implements TransactionInfoAdapter.TxI
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
MainActivity mainActivity = (MainActivity)getActivity();
mViewModel = new ViewModelProvider(this).get(HomeViewModel.class);
boolean usesPassword = PrefService.getInstance().getBoolean(Constants.PREF_USES_PASSWORD, false);
if(!usesPassword) {
bindObservers(view);
bindListeners(view);
} else {
mainActivity.restartEvents.observe(getViewLifecycleOwner(), o -> {
bindObservers(view);
bindListeners(view);
});
}
}
private void bindListeners(View view) {
@ -79,24 +92,30 @@ public class HomeFragment extends Fragment implements TransactionInfoAdapter.TxI
TextView unlockedBalanceTextView = view.findViewById(R.id.balance_unlocked_textview);
TextView lockedBalanceTextView = view.findViewById(R.id.balance_locked_textview);
BalanceService.getInstance().balance.observe(getViewLifecycleOwner(), balance -> {
BalanceService balanceService = BalanceService.getInstance();
HistoryService historyService = HistoryService.getInstance();
if(balanceService != null) {
balanceService.balance.observe(getViewLifecycleOwner(), balance -> {
unlockedBalanceTextView.setText(getString(R.string.wallet_balance_text, Wallet.getDisplayAmount(balance)));
});
BalanceService.getInstance().lockedBalance.observe(getViewLifecycleOwner(), lockedBalance -> {
if(lockedBalance == 0) {
balanceService.lockedBalance.observe(getViewLifecycleOwner(), lockedBalance -> {
if (lockedBalance == 0) {
lockedBalanceTextView.setVisibility(View.INVISIBLE);
} else {
lockedBalanceTextView.setText(getString(R.string.wallet_locked_balance_text, Wallet.getDisplayAmount(lockedBalance)));
lockedBalanceTextView.setVisibility(View.VISIBLE);
}
});
}
TransactionInfoAdapter adapter = new TransactionInfoAdapter(this);
txHistoryRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
txHistoryRecyclerView.setAdapter(adapter);
HistoryService.getInstance().history.observe(getViewLifecycleOwner(), history -> {
if(history.isEmpty()) {
if(historyService != null) {
historyService.history.observe(getViewLifecycleOwner(), history -> {
if (history.isEmpty()) {
txHistoryRecyclerView.setVisibility(View.GONE);
} else {
Collections.sort(history);
@ -105,6 +124,7 @@ public class HomeFragment extends Fragment implements TransactionInfoAdapter.TxI
}
});
}
}
@Override
public void onClickTransaction(TransactionInfo txInfo) {

View file

@ -0,0 +1,78 @@
package com.m2049r.xmrwallet.fragment.onboarding;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import com.m2049r.xmrwallet.MainActivity;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.service.PrefService;
import com.m2049r.xmrwallet.util.Constants;
import java.io.File;
public class OnboardingFragment extends Fragment {
private OnboardingViewModel mViewModel;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_settings, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mViewModel = new ViewModelProvider(this).get(OnboardingViewModel.class);
EditText walletPasswordEditText = view.findViewById(R.id.wallet_password_edittext);
EditText walletSeedEditText = view.findViewById(R.id.wallet_seed_edittext);
Button createWalletButton = view.findViewById(R.id.create_wallet_button);
createWalletButton.setOnClickListener(view1 -> {
String walletPassword = walletPasswordEditText.getText().toString();
if(!walletPassword.isEmpty()) {
PrefService.getInstance().edit().putBoolean(Constants.PREF_USES_PASSWORD, true).apply();
}
String walletSeed = walletSeedEditText.getText().toString().trim();
File walletFile = new File(getActivity().getApplicationInfo().dataDir, Constants.WALLET_NAME);
Wallet wallet = null;
if(walletSeed.isEmpty()) {
wallet = WalletManager.getInstance().createWallet(walletFile, walletPassword, Constants.MNEMONIC_LANGUAGE, 0);
} else {
wallet = WalletManager.getInstance().recoveryWallet(walletFile, walletPassword, walletSeed, "", 0);
}
wallet.close();
((MainActivity)getActivity()).init(walletFile, walletPassword);
getActivity().onBackPressed();
});
walletSeedEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
@Override
public void afterTextChanged(Editable editable) {
String text = editable.toString();
if(text.isEmpty()) {
createWalletButton.setText(R.string.create_wallet);
} else {
createWalletButton.setText(R.string.menu_restore);
}
}
});
}
}

View file

@ -0,0 +1,7 @@
package com.m2049r.xmrwallet.fragment.onboarding;
import androidx.lifecycle.ViewModel;
public class OnboardingViewModel extends ViewModel {
}

View file

@ -0,0 +1,19 @@
package com.m2049r.xmrwallet.service;
import android.content.Context;
import android.content.SharedPreferences;
import com.m2049r.xmrwallet.MainActivity;
public class PrefService extends ServiceBase {
public static SharedPreferences instance = null;
public static SharedPreferences getInstance() {
return instance;
}
public PrefService(MainActivity mainActivity) {
super(mainActivity, null);
instance = mainActivity.getSharedPreferences(mainActivity.getApplicationInfo().packageName, Context.MODE_PRIVATE);
}
}

View file

@ -10,18 +10,12 @@ public class TxService extends ServiceBase {
return instance;
}
private final SingleLiveEvent _clearSendEvent = new SingleLiveEvent();
public SingleLiveEvent clearSendEvent = _clearSendEvent;
public TxService(MainActivity mainActivity, MoneroHandlerThread thread) {
super(mainActivity, thread);
instance = this;
}
public void sendTx(String address, String amount, boolean sendAll) {
boolean success = this.getThread().sendTx(address, amount, sendAll);
if (success) {
_clearSendEvent.call();
}
public boolean sendTx(String address, String amount, boolean sendAll) {
return this.getThread().sendTx(address, amount, sendAll);
}
}

View file

@ -0,0 +1,7 @@
package com.m2049r.xmrwallet.util;
public class Constants {
public static final String WALLET_NAME = "xmr_wallet";
public static final String MNEMONIC_LANGUAGE = "English";
public static final String PREF_USES_PASSWORD = "pref_uses_password";
}

View file

@ -1,61 +0,0 @@
/*
* Copyright (c) 2018-2020 m2049r et al.
*
* 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.app.KeyguardManager;
import android.content.Context;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.os.CancellationSignal;
public class FingerprintHelper {
public static boolean isDeviceSupported(Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return false;
}
FingerprintManager fingerprintManager = context.getSystemService(FingerprintManager.class);
KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class);
return (keyguardManager != null) && (fingerprintManager != null) &&
keyguardManager.isKeyguardSecure() &&
fingerprintManager.isHardwareDetected() &&
fingerprintManager.hasEnrolledFingerprints();
}
public static boolean isFingerPassValid(Context context, String wallet) {
try {
KeyStoreHelper.loadWalletUserPass(context, wallet);
return true;
} catch (KeyStoreHelper.BrokenPasswordStoreException ex) {
return false;
}
}
public static void authenticate(Context context, CancellationSignal cancelSignal,
FingerprintManager.AuthenticationCallback callback) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return;
}
FingerprintManager manager = context.getSystemService(FingerprintManager.class);
if (manager != null) {
manager.authenticate(null, cancelSignal, 0, callback, null);
}
}
}

View file

@ -295,22 +295,6 @@ public class Helper {
return data;
}
static public void setMoneroHome(Context context) {
try {
String home = getStorage(context, MONERO_DIR).getAbsolutePath();
Os.setenv("HOME", home, true);
} catch (ErrnoException ex) {
throw new IllegalStateException(ex);
}
}
static public void initLogger(Context context) {
if (BuildConfig.DEBUG) {
initLogger(context, WalletManager.LOGLEVEL_DEBUG);
}
// no logger if not debug
}
// TODO make the log levels refer to the WalletManagerFactory::LogLevel enum ?
static public void initLogger(Context context, int level) {
String home = getStorage(context, MONERO_DIR).getAbsolutePath();
@ -318,88 +302,4 @@ public class Helper {
if (level >= WalletManager.LOGLEVEL_SILENT)
WalletManager.setLogLevel(level);
}
static public boolean useCrazyPass(Context context) {
File flagFile = new File(getWalletRoot(context), NOCRAZYPASS_FLAGFILE);
return !flagFile.exists();
}
// 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
static public String getWalletPassword(Context context, String walletName, String password) {
String walletPath = new File(getWalletRoot(context), walletName + ".keys").getAbsolutePath();
// try with entered password (which could be a legacy password or a CrAzYpass)
if (WalletManager.getInstance().verifyWalletPasswordOnly(walletPath, password)) {
return password;
}
// maybe this is a malformed CrAzYpass?
String possibleCrazyPass = CrazyPassEncoder.reformat(password);
if (possibleCrazyPass != null) { // looks like a CrAzYpass
if (WalletManager.getInstance().verifyWalletPasswordOnly(walletPath, possibleCrazyPass)) {
return possibleCrazyPass;
}
}
// generate & try with CrAzYpass
String crazyPass = KeyStoreHelper.getCrazyPass(context, password);
if (WalletManager.getInstance().verifyWalletPasswordOnly(walletPath, crazyPass)) {
return crazyPass;
}
// or maybe it is a broken CrAzYpass? (of which we have two variants)
String brokenCrazyPass2 = KeyStoreHelper.getBrokenCrazyPass(context, password, 2);
if ((brokenCrazyPass2 != null)
&& WalletManager.getInstance().verifyWalletPasswordOnly(walletPath, brokenCrazyPass2)) {
return brokenCrazyPass2;
}
String brokenCrazyPass1 = KeyStoreHelper.getBrokenCrazyPass(context, password, 1);
if ((brokenCrazyPass1 != null)
&& WalletManager.getInstance().verifyWalletPasswordOnly(walletPath, brokenCrazyPass1)) {
return brokenCrazyPass1;
}
return null;
}
static AlertDialog openDialog = null; // for preventing opening of multiple dialogs
static AsyncTask<Void, Void, Boolean> passwordTask = null;
public interface PasswordAction {
void act(String walletName, String password, boolean fingerprintUsed);
void fail(String walletName);
}
static private boolean processPasswordEntry(Context context, String walletName, String pass, boolean fingerprintUsed, PasswordAction action) {
String walletPassword = Helper.getWalletPassword(context, walletName, pass);
if (walletPassword != null) {
action.act(walletName, walletPassword, fingerprintUsed);
return true;
} else {
action.fail(walletName);
return false;
}
}
public interface Action {
boolean run();
}
static public boolean runWithNetwork(Action action) {
StrictMode.ThreadPolicy currentPolicy = StrictMode.getThreadPolicy();
StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitNetwork().build();
StrictMode.setThreadPolicy(policy);
try {
return action.run();
} finally {
StrictMode.setThreadPolicy(currentPolicy);
}
}
static public boolean preventScreenshot() {
return !(BuildConfig.DEBUG || BuildConfig.FLAVOR_type.equals("alpha"));
}
}

View file

@ -62,55 +62,6 @@ public class KeyStoreHelper {
System.loadLibrary("monerujo");
}
public static native byte[] slowHash(byte[] data, int brokenVariant);
static final private String RSA_ALIAS = "MonerujoRSA";
private static String getCrazyPass(Context context, String password, int brokenVariant) {
byte[] data = password.getBytes(StandardCharsets.UTF_8);
byte[] sig = null;
try {
KeyStoreHelper.createKeys(context, RSA_ALIAS);
sig = KeyStoreHelper.signData(RSA_ALIAS, data);
byte[] hash = slowHash(sig, brokenVariant);
if (hash == null) {
throw new IllegalStateException("Slow Hash is null!");
}
return CrazyPassEncoder.encode(hash);
} catch (NoSuchProviderException | NoSuchAlgorithmException |
InvalidAlgorithmParameterException | KeyStoreException |
InvalidKeyException | SignatureException ex) {
throw new IllegalStateException(ex);
}
}
public static String getCrazyPass(Context context, String password) {
if (Helper.useCrazyPass(context))
return getCrazyPass(context, password, 0);
else
return password;
}
public static String getBrokenCrazyPass(Context context, String password, int brokenVariant) {
// due to a link bug in the initial implementation, some crazypasses were built with
// prehash & variant == 1
// since there are wallets out there, we need to keep this here
// yes, it's a mess
if (isArm32() && (brokenVariant != 2)) return null;
return getCrazyPass(context, password, brokenVariant);
}
private static Boolean isArm32 = null;
public static boolean isArm32() {
if (isArm32 != null) return isArm32;
synchronized (KeyStoreException.class) {
if (isArm32 != null) return isArm32;
isArm32 = Build.SUPPORTED_ABIS[0].equals("armeabi-v7a");
return isArm32;
}
}
public static boolean saveWalletUserPass(@NonNull Context context, String wallet, String password) {
String walletKeyAlias = SecurityConstants.WALLET_PASS_KEY_PREFIX + wallet;
byte[] data = password.getBytes(StandardCharsets.UTF_8);
@ -141,38 +92,6 @@ public class KeyStoreHelper {
}
}
public static boolean hasStoredPasswords(@NonNull Context context) {
SharedPreferences prefs = context.getSharedPreferences(SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE);
return prefs.getAll().size() > 0;
}
public static String loadWalletUserPass(@NonNull Context context, String wallet) throws BrokenPasswordStoreException {
String walletKeyAlias = SecurityConstants.WALLET_PASS_KEY_PREFIX + wallet;
String encoded = context.getSharedPreferences(SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE)
.getString(wallet, "");
if (encoded.isEmpty()) throw new BrokenPasswordStoreException();
byte[] data = Base64.decode(encoded, Base64.DEFAULT);
byte[] decrypted = KeyStoreHelper.decrypt(walletKeyAlias, data);
if (decrypted == null) throw new BrokenPasswordStoreException();
return new String(decrypted, StandardCharsets.UTF_8);
}
public static void removeWalletUserPass(Context context, String wallet) {
String walletKeyAlias = SecurityConstants.WALLET_PASS_KEY_PREFIX + wallet;
try {
KeyStoreHelper.deleteKeys(walletKeyAlias);
} catch (KeyStoreException ex) {
Timber.w(ex);
}
context.getSharedPreferences(SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE).edit()
.remove(wallet).apply();
}
public static void copyWalletUserPass(Context context, String srcWallet, String dstWallet) throws BrokenPasswordStoreException {
final String pass = loadWalletUserPass(context, srcWallet);
saveWalletUserPass(context, dstWallet, pass);
}
/**
* 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.

View file

@ -6,14 +6,40 @@
android:layout_height="match_parent"
tools:context=".fragment.settings.SettingsFragment">
<ImageView
android:id="@+id/monero_logo_imageview"
android:layout_width="wrap_content"
<EditText
android:id="@+id/wallet_password_edittext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:src="@drawable/ic_monero_qr"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="24dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="32dp"
android:hint="Password (optional)"
android:inputType="textPassword"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/wallet_seed_edittext"
tools:visibility="visible"/>
<EditText
android:id="@+id/wallet_seed_edittext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="12dp"
android:hint="Recovery phrase (optional)"
android:inputType="textPassword"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/create_wallet_button"
tools:visibility="visible"/>
<Button
android:id="@+id/create_wallet_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:layout_marginTop="32dp"
android:text="@string/create_wallet"
app:layout_constraintTop_toBottomOf="@id/wallet_seed_edittext"
app:layout_constraintStart_toStartOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:fitsSystemWindows="true"
android:padding="16dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/wallet_password_edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="32dp"
android:hint="Password"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/unlock_wallet_button"/>
<ImageButton
android:id="@+id/paste_password_imagebutton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:padding="8dp"
android:src="@drawable/ic_content_paste_24dp"
app:layout_constraintTop_toTopOf="@id/wallet_password_edittext"
app:layout_constraintBottom_toBottomOf="@id/wallet_password_edittext"
app:layout_constraintEnd_toEndOf="@id/wallet_password_edittext"/>
<Button
android:id="@+id/unlock_wallet_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:layout_marginTop="32dp"
android:text="Unlock"
app:layout_constraintTop_toBottomOf="@id/wallet_password_edittext"
app:layout_constraintStart_toStartOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -16,6 +16,13 @@
android:name="address"
app:argType="string" />
</action>
<action
android:id="@+id/nav_to_onboarding"
app:destination="@id/onboarding_fragment">
<argument
android:name="address"
app:argType="string" />
</action>
</fragment>
<fragment
android:id="@+id/settings_fragment"
@ -23,4 +30,10 @@
android:label="fragment_send_amount"
tools:layout="@layout/fragment_settings">
</fragment>
<fragment
android:id="@+id/onboarding_fragment"
android:name="com.m2049r.xmrwallet.fragment.onboarding.OnboardingFragment"
android:label="fragment_onboarding"
tools:layout="@layout/fragment_settings">
</fragment>
</navigation>

View file

@ -541,4 +541,7 @@
<string name="send_amount_invalid">Please enter a valid amount</string>
<string name="send_max">Send Max</string>
<string name="undo">Undo</string>
<string name="error_sending_tx">Error sending tx</string>
<string name="create_wallet">Create wallet</string>
<string name="invalid_mnemonic_code">Invalid mnemonic</string>
</resources>