diff --git a/app/build.gradle b/app/build.gradle index a55f882..f1f71bc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.m2049r.xmrwallet" minSdkVersion 21 targetSdkVersion 25 - versionCode 102 - versionName "1.5.12 'Maximum Nachos'" + versionCode 111 + versionName "1.6.1 'Nano S'" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" externalNativeBuild { cmake { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4bf5b3a..8919738 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,11 +10,11 @@ + + + + + + + + FindClass("com/m2049r/xmrwallet/model/Transfer"))); class_WalletListener = static_cast(jenv->NewGlobalRef( jenv->FindClass("com/m2049r/xmrwallet/model/WalletListener"))); + class_Ledger = static_cast(jenv->NewGlobalRef( + jenv->FindClass("com/m2049r/xmrwallet/ledger/Ledger"))); return JNI_VERSION_1_6; } #ifdef __cplusplus @@ -353,6 +356,39 @@ Java_com_m2049r_xmrwallet_model_WalletManager_createWalletFromKeysJ(JNIEnv *env, return reinterpret_cast(wallet); } + +// virtual void setSubaddressLookahead(uint32_t major, uint32_t minor) = 0; + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_createWalletFromDeviceJ(JNIEnv *env, jobject instance, + jstring path, + jstring password, + jint networkType, + jstring deviceName, + jlong restoreHeight, + jstring subaddressLookahead) { + const char *_path = env->GetStringUTFChars(path, NULL); + const char *_password = env->GetStringUTFChars(password, NULL); + Monero::NetworkType _networkType = static_cast(networkType); + const char *_deviceName = env->GetStringUTFChars(deviceName, NULL); + const char *_subaddressLookahead = env->GetStringUTFChars(subaddressLookahead, NULL); + + Bitmonero::Wallet *wallet = + Bitmonero::WalletManagerFactory::getWalletManager()->createWalletFromDevice( + std::string(_path), + std::string(_password), + _networkType, + std::string(_deviceName), + (uint64_t) restoreHeight, + std::string(_subaddressLookahead)); + + env->ReleaseStringUTFChars(path, _path); + env->ReleaseStringUTFChars(password, _password); + env->ReleaseStringUTFChars(deviceName, _deviceName); + env->ReleaseStringUTFChars(subaddressLookahead, _subaddressLookahead); + return reinterpret_cast(wallet); +} + JNIEXPORT jboolean JNICALL Java_com_m2049r_xmrwallet_model_WalletManager_walletExists(JNIEnv *env, jobject instance, jstring path) { @@ -378,6 +414,20 @@ Java_com_m2049r_xmrwallet_model_WalletManager_verifyWalletPassword(JNIEnv *env, return static_cast(passwordOk); } +//virtual int queryWalletHardware(const std::string &keys_file_name, const std::string &password) const = 0; +JNIEXPORT jint JNICALL +Java_com_m2049r_xmrwallet_model_WalletManager_queryWalletHardware(JNIEnv *env, jobject instance, + jstring keys_file_name, + jstring password) { + const char *_keys_file_name = env->GetStringUTFChars(keys_file_name, NULL); + const char *_password = env->GetStringUTFChars(password, NULL); + int hardwareId = + Bitmonero::WalletManagerFactory::getWalletManager()-> + queryWalletHardware(std::string(_keys_file_name), std::string(_password)); + env->ReleaseStringUTFChars(keys_file_name, _keys_file_name); + env->ReleaseStringUTFChars(password, _password); + return static_cast(hardwareId); +} JNIEXPORT jobject JNICALL Java_com_m2049r_xmrwallet_model_WalletManager_findWallets(JNIEnv *env, jobject instance, @@ -712,6 +762,13 @@ Java_com_m2049r_xmrwallet_model_Wallet_isSynchronized(JNIEnv *env, jobject insta return static_cast(wallet->synchronized()); } +JNIEXPORT jboolean JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_isKeyOnDevice(JNIEnv *env, jobject instance) { + Bitmonero::Wallet *wallet = getHandle(env, instance); + bool key_on_device = wallet->isKeyOnDevice(); + return static_cast(key_on_device); +} + //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_slowHash(JNIEnv *env, jobject clazz, @@ -1309,6 +1366,96 @@ Java_com_m2049r_xmrwallet_model_WalletManager_setLogLevel(JNIEnv *env, jobject i Bitmonero::WalletManagerFactory::setLogLevel(level); } +// +// Ledger Stuff +// + +#include "monerujo_ledger.h" + +/** + * @brief LedgerExchange - exchange data with Ledger Device + * @param pbSendBuffer - buffer for data to send + * @param cbSendLength - length of send buffer + * @param pbRecvBuffer - buffer for received data + * @param pcbRecvLength - pointer to size of receive buffer + * gets set with length of received data on successful return + * @return SCARD_S_SUCCESS - success + * SCARD_E_NO_READERS_AVAILABLE - no device connected / found + * SCARD_E_INSUFFICIENT_BUFFER - pbRecvBuffer is too small for the response + */ +LONG LedgerExchange( + LPCBYTE pbSendBuffer, + DWORD cbSendLength, + LPBYTE pbRecvBuffer, + LPDWORD pcbRecvLength) { + LOGD("LedgerExchange"); + JNIEnv *jenv; + int envStat = attachJVM(&jenv); + if (envStat == JNI_ERR) return -1; + + jmethodID exchangeMethod = jenv->GetStaticMethodID(class_Ledger, "Exchange", "([B)[B"); + + jsize sendLen = static_cast(cbSendLength); + jbyteArray dataSend = jenv->NewByteArray(sendLen); + jenv->SetByteArrayRegion(dataSend, 0, sendLen, (jbyte *) pbSendBuffer); + jbyteArray dataRecv = (jbyteArray) jenv->CallStaticObjectMethod(class_Ledger, exchangeMethod, + dataSend); + jenv->DeleteLocalRef(dataSend); + if (dataRecv == nullptr) { + detachJVM(jenv, envStat); + LOGD("LedgerExchange SCARD_E_NO_READERS_AVAILABLE"); + return SCARD_E_NO_READERS_AVAILABLE; + } + jsize len = jenv->GetArrayLength(dataRecv); + LOGD("LedgerExchange SCARD_S_SUCCESS %ld/%d", cbSendLength, len); + if (len <= *pcbRecvLength) { + *pcbRecvLength = static_cast(len); + jenv->GetByteArrayRegion(dataRecv, 0, len, (jbyte *) pbRecvBuffer); + jenv->DeleteLocalRef(dataRecv); + detachJVM(jenv, envStat); + return SCARD_S_SUCCESS; + } else { + jenv->DeleteLocalRef(dataRecv); + detachJVM(jenv, envStat); + LOGE("LedgerExchange SCARD_E_INSUFFICIENT_BUFFER"); + return SCARD_E_INSUFFICIENT_BUFFER; + } +} + +/** + * @brief LedgerFind - find Ledger Device and return it's name + * @param buffer - buffer for name of found device + * @param len - length of buffer + * @return 0 - success + * -1 - no device connected / found + * -2 - JVM not found + */ +int LedgerFind(char *buffer, size_t len) { + LOGD("LedgerName"); + JNIEnv *jenv; + int envStat = attachJVM(&jenv); + if (envStat == JNI_ERR) return -2; + + jmethodID nameMethod = jenv->GetStaticMethodID(class_Ledger, "Name", "()Ljava/lang/String;"); + jstring name = (jstring) jenv->CallStaticObjectMethod(class_Ledger, nameMethod); + + int ret; + if (name != nullptr) { + const char *_name = jenv->GetStringUTFChars(name, NULL); + strncpy(buffer, _name, len); + jenv->ReleaseStringUTFChars(name, _name); + buffer[len - 1] = 0; // terminate in case _name is bigger + ret = 0; + LOGD("LedgerName is %s", buffer); + } else { + buffer[0] = 0; + ret = -1; + } + + detachJVM(jenv, envStat); + return ret; +} + #ifdef __cplusplus } #endif diff --git a/app/src/main/cpp/monerujo_ledger.h b/app/src/main/cpp/monerujo_ledger.h new file mode 100644 index 0000000..7e523bf --- /dev/null +++ b/app/src/main/cpp/monerujo_ledger.h @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2018 m2049r + *

+ * 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. + */ + +#ifndef XMRWALLET_LEDGER_H +#define XMRWALLET_LEDGER_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#define SCARD_S_SUCCESS ((LONG)0x00000000) /**< No error was encountered. */ +#define SCARD_E_INSUFFICIENT_BUFFER ((LONG)0x80100008) /**< The data buffer to receive returned data is too small for the returned data. */ +#define SCARD_E_NO_READERS_AVAILABLE ((LONG)0x8010002E) /**< Cannot find a smart card reader. */ + +typedef long LONG; +typedef unsigned long DWORD; +typedef DWORD *LPDWORD; +typedef unsigned char BYTE; +typedef BYTE *LPBYTE; +typedef const BYTE *LPCBYTE; + +typedef char CHAR; +typedef CHAR *LPSTR; + +int LedgerFind(char *buffer, size_t len); +LONG LedgerExchange(LPCBYTE pbSendBuffer, DWORD cbSendLength, LPBYTE pbRecvBuffer, LPDWORD pcbRecvLength); + +#ifdef __cplusplus +} +#endif + +#endif //XMRWALLET_LEDGER_H diff --git a/app/src/main/java/com/btchip/BTChipException.java b/app/src/main/java/com/btchip/BTChipException.java new file mode 100644 index 0000000..440a3ad --- /dev/null +++ b/app/src/main/java/com/btchip/BTChipException.java @@ -0,0 +1,53 @@ +/* + ******************************************************************************* + * BTChip Bitcoin Hardware Wallet Java API + * (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn + * + * 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.btchip; + +public class BTChipException extends Exception { + + private static final long serialVersionUID = 5512803003827126405L; + + public BTChipException(String reason) { + super(reason); + } + + public BTChipException(String reason, Throwable cause) { + super(reason, cause); + } + + public BTChipException(String reason, int sw) { + super(reason); + this.sw = sw; + } + + public int getSW() { + return sw; + } + + public String toString() { + if (sw == 0) { + return "BTChip Exception : " + getMessage(); + } else { + return "BTChip Exception : " + getMessage() + " " + Integer.toHexString(sw); + } + } + + private int sw; + +} diff --git a/app/src/main/java/com/btchip/comm/BTChipTransport.java b/app/src/main/java/com/btchip/comm/BTChipTransport.java new file mode 100644 index 0000000..5fe921f --- /dev/null +++ b/app/src/main/java/com/btchip/comm/BTChipTransport.java @@ -0,0 +1,31 @@ +/* + ******************************************************************************* + * BTChip Bitcoin Hardware Wallet Java API + * (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn + * (c) 2018 m2049r + * + * 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.btchip.comm; + +import com.btchip.BTChipException; + +public interface BTChipTransport { + public byte[] exchange(byte[] command); + + public void close(); + + public void setDebug(boolean debugFlag); +} diff --git a/app/src/main/java/com/btchip/comm/LedgerHelper.java b/app/src/main/java/com/btchip/comm/LedgerHelper.java new file mode 100644 index 0000000..db24899 --- /dev/null +++ b/app/src/main/java/com/btchip/comm/LedgerHelper.java @@ -0,0 +1,126 @@ +/* + ******************************************************************************* + * BTChip Bitcoin Hardware Wallet Java API + * (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn + * (c) 2018 m2049r + * + * 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.btchip.comm; + +import java.io.ByteArrayOutputStream; + +public class LedgerHelper { + + private static final int TAG_APDU = 0x05; + + public static byte[] wrapCommandAPDU(int channel, byte[] command, int packetSize) { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + if (packetSize < 3) { + throw new IllegalArgumentException("Can't handle Ledger framing with less than 3 bytes for the report"); + } + int sequenceIdx = 0; + int offset = 0; + output.write(channel >> 8); + output.write(channel); + output.write(TAG_APDU); + output.write(sequenceIdx >> 8); + output.write(sequenceIdx); + sequenceIdx++; + output.write(command.length >> 8); + output.write(command.length); + int blockSize = (command.length > packetSize - 7 ? packetSize - 7 : command.length); + output.write(command, offset, blockSize); + offset += blockSize; + while (offset != command.length) { + output.write(channel >> 8); + output.write(channel); + output.write(TAG_APDU); + output.write(sequenceIdx >> 8); + output.write(sequenceIdx); + sequenceIdx++; + blockSize = (command.length - offset > packetSize - 5 ? packetSize - 5 : command.length - offset); + output.write(command, offset, blockSize); + offset += blockSize; + } + if ((output.size() % packetSize) != 0) { + byte[] padding = new byte[packetSize - (output.size() % packetSize)]; + output.write(padding, 0, padding.length); + } + return output.toByteArray(); + } + + public static byte[] unwrapResponseAPDU(int channel, byte[] data, int packetSize) { + ByteArrayOutputStream response = new ByteArrayOutputStream(); + int offset = 0; + int responseLength; + int sequenceIdx = 0; + if ((data == null) || (data.length < 7 + 5)) { + return null; + } + if (data[offset++] != (channel >> 8)) { + throw new IllegalArgumentException("Invalid channel"); + } + if (data[offset++] != (channel & 0xff)) { + throw new IllegalArgumentException("Invalid channel"); + } + if (data[offset++] != TAG_APDU) { + throw new IllegalArgumentException("Invalid tag"); + } + if (data[offset++] != 0x00) { + throw new IllegalArgumentException("Invalid sequence"); + } + if (data[offset++] != 0x00) { + throw new IllegalArgumentException("Invalid sequence"); + } + responseLength = ((data[offset++] & 0xff) << 8); + responseLength |= (data[offset++] & 0xff); + if (data.length < 7 + responseLength) { + return null; + } + int blockSize = (responseLength > packetSize - 7 ? packetSize - 7 : responseLength); + response.write(data, offset, blockSize); + offset += blockSize; + while (response.size() != responseLength) { + sequenceIdx++; + if (offset == data.length) { + return null; + } + if (data[offset++] != (channel >> 8)) { + throw new IllegalArgumentException("Invalid channel"); + } + if (data[offset++] != (channel & 0xff)) { + throw new IllegalArgumentException("Invalid channel"); + } + if (data[offset++] != TAG_APDU) { + throw new IllegalArgumentException("Invalid tag"); + } + if (data[offset++] != (sequenceIdx >> 8)) { + throw new IllegalArgumentException("Invalid sequence"); + } + if (data[offset++] != (sequenceIdx & 0xff)) { + throw new IllegalArgumentException("Invalid sequence"); + } + blockSize = (responseLength - response.size() > packetSize - 5 ? packetSize - 5 : responseLength - response.size()); + if (blockSize > data.length - offset) { + return null; + } + response.write(data, offset, blockSize); + offset += blockSize; + } + return response.toByteArray(); + } + +} diff --git a/app/src/main/java/com/btchip/comm/android/BTChipTransportAndroidHID.java b/app/src/main/java/com/btchip/comm/android/BTChipTransportAndroidHID.java new file mode 100644 index 0000000..e4cbd43 --- /dev/null +++ b/app/src/main/java/com/btchip/comm/android/BTChipTransportAndroidHID.java @@ -0,0 +1,145 @@ +/* + ******************************************************************************* + * BTChip Bitcoin Hardware Wallet Java API + * (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn + * (c) 2018 m2049r + * + * 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.btchip.comm.android; + +import android.hardware.usb.UsbConstants; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbEndpoint; +import android.hardware.usb.UsbInterface; +import android.hardware.usb.UsbManager; +import android.hardware.usb.UsbRequest; + +import com.btchip.BTChipException; +import com.btchip.comm.BTChipTransport; +import com.btchip.comm.LedgerHelper; +import com.btchip.utils.Dump; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.HashMap; + +import timber.log.Timber; + +public class BTChipTransportAndroidHID implements BTChipTransport { + + public static UsbDevice getDevice(UsbManager manager) { + HashMap deviceList = manager.getDeviceList(); + for (UsbDevice device : deviceList.values()) { + Timber.d("%04X:%04X %s, %s", device.getVendorId(), device.getProductId(), device.getManufacturerName(), device.getProductName()); + if ((device.getVendorId() == VID) && (device.getProductId() == PID_HID)) { + return device; + } + } + return null; + } + + public static BTChipTransport open(UsbManager manager, UsbDevice device) throws IOException { + UsbDeviceConnection connection = manager.openDevice(device); + if (connection == null) throw new IOException("Device not connected"); + // Must only be called once permission is granted (see http://developer.android.com/reference/android/hardware/usb/UsbManager.html) + // Important if enumerating, rather than being awaken by the intent notification + UsbInterface dongleInterface = device.getInterface(0); + UsbEndpoint in = null; + UsbEndpoint out = null; + for (int i = 0; i < dongleInterface.getEndpointCount(); i++) { + UsbEndpoint tmpEndpoint = dongleInterface.getEndpoint(i); + if (tmpEndpoint.getDirection() == UsbConstants.USB_DIR_IN) { + in = tmpEndpoint; + } else { + out = tmpEndpoint; + } + } + connection.claimInterface(dongleInterface, true); + return new BTChipTransportAndroidHID(connection, dongleInterface, in, out); + } + + private static final int VID = 0x2C97; + private static final int PID_HID = 0x0001; + + private UsbDeviceConnection connection; + private UsbInterface dongleInterface; + private UsbEndpoint in; + private UsbEndpoint out; + private byte transferBuffer[]; + private boolean debug; + + public BTChipTransportAndroidHID(UsbDeviceConnection connection, UsbInterface dongleInterface, UsbEndpoint in, UsbEndpoint out) { + this.connection = connection; + this.dongleInterface = dongleInterface; + this.in = in; + this.out = out; + transferBuffer = new byte[HID_BUFFER_SIZE]; + } + + @Override + public byte[] exchange(byte[] command) { + ByteArrayOutputStream response = new ByteArrayOutputStream(); + byte[] responseData = null; + int offset = 0; + if (debug) { + Timber.d("=> %s", Dump.dump(command)); + } + command = LedgerHelper.wrapCommandAPDU(LEDGER_DEFAULT_CHANNEL, command, HID_BUFFER_SIZE); + UsbRequest requestOut = new UsbRequest(); + requestOut.initialize(connection, out); + while (offset != command.length) { + int blockSize = (command.length - offset > HID_BUFFER_SIZE ? HID_BUFFER_SIZE : command.length - offset); + System.arraycopy(command, offset, transferBuffer, 0, blockSize); + requestOut.queue(ByteBuffer.wrap(transferBuffer), HID_BUFFER_SIZE); + connection.requestWait(); + offset += blockSize; + } + requestOut.close(); + ByteBuffer responseBuffer = ByteBuffer.allocate(HID_BUFFER_SIZE); + UsbRequest requestIn = new UsbRequest(); + requestIn.initialize(connection, in); + while ((responseData = LedgerHelper.unwrapResponseAPDU(LEDGER_DEFAULT_CHANNEL, response.toByteArray(), HID_BUFFER_SIZE)) == null) { + responseBuffer.clear(); + requestIn.queue(responseBuffer, HID_BUFFER_SIZE); + connection.requestWait(); + responseBuffer.rewind(); + responseBuffer.get(transferBuffer, 0, HID_BUFFER_SIZE); + response.write(transferBuffer, 0, HID_BUFFER_SIZE); + } + requestIn.close(); + if (debug) { + Timber.d("<= %s", Dump.dump(responseData)); + } + return responseData; + } + + @Override + public void close() { + connection.releaseInterface(dongleInterface); + connection.close(); + } + + @Override + public void setDebug(boolean debugFlag) { + this.debug = debugFlag; + } + + private static final int HID_BUFFER_SIZE = 64; + private static final int LEDGER_DEFAULT_CHANNEL = 1; + private static final int SW1_DATA_AVAILABLE = 0x61; +} diff --git a/app/src/main/java/com/btchip/utils/Dump.java b/app/src/main/java/com/btchip/utils/Dump.java new file mode 100644 index 0000000..2d453fc --- /dev/null +++ b/app/src/main/java/com/btchip/utils/Dump.java @@ -0,0 +1,62 @@ +/* + ******************************************************************************* + * BTChip Bitcoin Hardware Wallet Java API + * (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn + * + * 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.btchip.utils; + +import java.io.ByteArrayOutputStream; + +public class Dump { + + public static String dump(byte[] buffer, int offset, int length) { + String result = ""; + for (int i = 0; i < length; i++) { + String temp = Integer.toHexString((buffer[offset + i]) & 0xff); + if (temp.length() < 2) { + temp = "0" + temp; + } + result += temp; + } + return result; + } + + public static String dump(byte[] buffer) { + return dump(buffer, 0, buffer.length); + } + + public static byte[] hexToBin(String src) { + ByteArrayOutputStream result = new ByteArrayOutputStream(); + int i = 0; + while (i < src.length()) { + char x = src.charAt(i); + if (!((x >= '0' && x <= '9') || (x >= 'A' && x <= 'F') || (x >= 'a' && x <= 'f'))) { + i++; + continue; + } + try { + result.write(Integer.valueOf("" + src.charAt(i) + src.charAt(i + 1), 16)); + i += 2; + } catch (Exception e) { + return null; + } + } + return result.toByteArray(); + } + + +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/BaseActivity.java b/app/src/main/java/com/m2049r/xmrwallet/BaseActivity.java new file mode 100644 index 0000000..8ef124d --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/BaseActivity.java @@ -0,0 +1,105 @@ +package com.m2049r.xmrwallet; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.os.PowerManager; + +import com.m2049r.xmrwallet.dialog.ProgressDialog; +import com.m2049r.xmrwallet.ledger.Ledger; +import com.m2049r.xmrwallet.ledger.LedgerProgressDialog; + +import timber.log.Timber; + +public class BaseActivity extends SecureActivity implements GenerateReviewFragment.ProgressListener { + + ProgressDialog progressDialog = null; + + private class SimpleProgressDialog extends ProgressDialog { + + SimpleProgressDialog(Context context, int msgId) { + super(context); + setCancelable(false); + setMessage(context.getString(msgId)); + } + + @Override + public void onBackPressed() { + // prevent back button + } + } + + @Override + public void showProgressDialog(int msgId) { + showProgressDialog(msgId, 0); + } + + public void showProgressDialog(int msgId, long delay) { + dismissProgressDialog(); // just in case + progressDialog = new SimpleProgressDialog(BaseActivity.this, msgId); + if (delay > 0) { + Handler handler = new Handler(); + handler.postDelayed(new Runnable() { + public void run() { + if (progressDialog != null) progressDialog.show(); + } + }, delay); + } else { + progressDialog.show(); + } + } + + @Override + public void showLedgerProgressDialog(int mode) { + dismissProgressDialog(); // just in case + progressDialog = new LedgerProgressDialog(BaseActivity.this, mode); + Ledger.setListener((Ledger.Listener) progressDialog); + progressDialog.show(); + } + + @Override + public void dismissProgressDialog() { + if (progressDialog == null) return; // nothing to do + if (progressDialog instanceof Ledger.Listener) { + Ledger.unsetListener((Ledger.Listener) progressDialog); + } + if (progressDialog.isShowing()) { + progressDialog.dismiss(); + } + progressDialog = null; + } + + static final int RELEASE_WAKE_LOCK_DELAY = 5000; // millisconds + + private PowerManager.WakeLock wl = null; + + void acquireWakeLock() { + if ((wl != null) && wl.isHeld()) return; + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + this.wl = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, getString(R.string.app_name)); + try { + wl.acquire(); + Timber.d("WakeLock acquired"); + } catch (SecurityException ex) { + Timber.w("WakeLock NOT acquired: %s", ex.getLocalizedMessage()); + wl = null; + } + } + + void releaseWakeLock(int delayMillis) { + Handler handler = new Handler(Looper.getMainLooper()); + handler.postDelayed(new Runnable() { + @Override + public void run() { + releaseWakeLock(); + } + }, delayMillis); + } + + void releaseWakeLock() { + if ((wl == null) || !wl.isHeld()) return; + wl.release(); + wl = null; + Timber.d("WakeLock released"); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java b/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java index 5a14739..36acae4 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java @@ -61,6 +61,7 @@ public class GenerateFragment extends Fragment { static final String TYPE_NEW = "new"; static final String TYPE_KEY = "key"; static final String TYPE_SEED = "seed"; + static final String TYPE_LEDGER = "ledger"; static final String TYPE_VIEWONLY = "view"; private TextInputLayout etWalletName; @@ -190,6 +191,17 @@ public class GenerateFragment extends Fragment { return false; } }); + } else if (type.equals(TYPE_LEDGER)) { + etWalletPassword.getEditText().setImeOptions(EditorInfo.IME_ACTION_DONE); + etWalletPassword.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)) { + etWalletRestoreHeight.requestFocus(); + return true; + } + return false; + } + }); } else if (type.equals(TYPE_SEED)) { etWalletPassword.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() { public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { @@ -485,6 +497,12 @@ public class GenerateFragment extends Fragment { KeyStoreHelper.saveWalletUserPass(getActivity(), name, password); } activityCallback.onGenerate(name, crazyPass, seed, height); + } else if (type.equals(TYPE_LEDGER)) { + bGenerate.setEnabled(false); + if (fingerprintAuthAllowed) { + KeyStoreHelper.saveWalletUserPass(getActivity(), name, password); + } + activityCallback.onGenerateLedger(name, crazyPass, height); } else if (type.equals(TYPE_KEY) || type.equals(TYPE_VIEWONLY)) { if (checkAddress() && checkViewKey() && checkSpendKey()) { bGenerate.setEnabled(false); @@ -523,6 +541,8 @@ public class GenerateFragment extends Fragment { return getString(R.string.generate_wallet_type_new); case TYPE_SEED: return getString(R.string.generate_wallet_type_seed); + case TYPE_LEDGER: + return getString(R.string.generate_wallet_type_ledger); case TYPE_VIEWONLY: return getString(R.string.generate_wallet_type_view); default: @@ -540,6 +560,8 @@ public class GenerateFragment extends Fragment { void onGenerate(String name, String password, String address, String viewKey, String spendKey, long height); + void onGenerateLedger(String name, String password, long height); + void setTitle(String title); void setToolbarButton(int type); @@ -575,6 +597,9 @@ public class GenerateFragment extends Fragment { case TYPE_SEED: inflater.inflate(R.menu.create_wallet_seed, menu); break; + case TYPE_LEDGER: + inflater.inflate(R.menu.create_wallet_ledger, menu); + break; case TYPE_VIEWONLY: inflater.inflate(R.menu.create_wallet_view, menu); break; diff --git a/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java b/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java index 6f44246..034b507 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java @@ -43,6 +43,8 @@ import android.widget.Switch; import android.widget.TextView; import android.widget.Toast; +import com.m2049r.xmrwallet.ledger.Ledger; +import com.m2049r.xmrwallet.ledger.LedgerProgressDialog; import com.m2049r.xmrwallet.model.NetworkType; import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.model.WalletManager; @@ -52,8 +54,6 @@ import com.m2049r.xmrwallet.util.KeyStoreHelper; import com.m2049r.xmrwallet.util.MoneroThreadPoolExecutor; import com.m2049r.xmrwallet.widget.Toolbar; -import java.io.File; - import timber.log.Timber; public class GenerateReviewFragment extends Fragment { @@ -76,6 +76,9 @@ public class GenerateReviewFragment extends Fragment { private ImageButton bCopyAddress; private LinearLayout llAdvancedInfo; private LinearLayout llPassword; + private LinearLayout llMnemonic; + private LinearLayout llSpendKey; + private LinearLayout llViewKey; private Button bAdvancedInfo; private Button bAccept; @@ -99,6 +102,9 @@ public class GenerateReviewFragment extends Fragment { bAdvancedInfo = (Button) view.findViewById(R.id.bAdvancedInfo); llAdvancedInfo = (LinearLayout) view.findViewById(R.id.llAdvancedInfo); llPassword = (LinearLayout) view.findViewById(R.id.llPassword); + llMnemonic = (LinearLayout) view.findViewById(R.id.llMnemonic); + llSpendKey = (LinearLayout) view.findViewById(R.id.llSpendKey); + llViewKey = (LinearLayout) view.findViewById(R.id.llViewKey); bAccept = (Button) view.findViewById(R.id.bAccept); @@ -142,7 +148,6 @@ public class GenerateReviewFragment extends Fragment { } void showDetails() { - showProgress(); tvWalletPassword.setText(null); new AsyncShow().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR, walletPath); } @@ -188,6 +193,20 @@ public class GenerateReviewFragment extends Fragment { boolean isWatchOnly; Wallet.Status status; + boolean dialogOpened = false; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + showProgress(); + if ((walletPath != null) + && (WalletManager.getInstance().queryWalletHardware(walletPath + ".keys", getPassword()) == 1) + && (progressCallback != null)) { + progressCallback.showLedgerProgressDialog(LedgerProgressDialog.TYPE_RESTORE); + dialogOpened = true; + } + } + @Override protected Boolean doInBackground(String... params) { if (params.length != 1) return false; @@ -212,7 +231,11 @@ public class GenerateReviewFragment extends Fragment { address = wallet.getAddress(); seed = wallet.getSeed(); - viewKey = wallet.getSecretViewKey(); + if (wallet.isKeyOnDevice()) { + viewKey = Ledger.Key(); + } else { + viewKey = wallet.getSecretViewKey(); + } spendKey = isWatchOnly ? getActivity().getString(R.string.label_watchonly) : wallet.getSecretSpendKey(); isWatchOnly = wallet.isWatchOnly(); if (closeWallet) wallet.close(); @@ -222,6 +245,8 @@ public class GenerateReviewFragment extends Fragment { @Override protected void onPostExecute(Boolean result) { super.onPostExecute(result); + if (dialogOpened) + progressCallback.dismissProgressDialog(); if (!isAdded()) return; // never mind walletName = name; if (result) { @@ -232,10 +257,22 @@ public class GenerateReviewFragment extends Fragment { llPassword.setVisibility(View.VISIBLE); tvWalletPassword.setText(getPassword()); tvWalletAddress.setText(address); - tvWalletMnemonic.setText(seed); - tvWalletViewKey.setText(viewKey); - tvWalletSpendKey.setText(spendKey); - bAdvancedInfo.setVisibility(View.VISIBLE); + if (!seed.isEmpty()) { + llMnemonic.setVisibility(View.VISIBLE); + tvWalletMnemonic.setText(seed); + } + boolean showAdvanced = false; + if (isKeyValid(viewKey)) { + llViewKey.setVisibility(View.VISIBLE); + tvWalletViewKey.setText(viewKey); + showAdvanced = true; + } + if (isKeyValid(spendKey)) { + llSpendKey.setVisibility(View.VISIBLE); + tvWalletSpendKey.setText(spendKey); + showAdvanced = true; + } + if (showAdvanced) bAdvancedInfo.setVisibility(View.VISIBLE); bCopyAddress.setClickable(true); bCopyAddress.setImageResource(R.drawable.ic_content_copy_black_24dp); activityCallback.setTitle(name, getString(R.string.details_title)); @@ -267,6 +304,8 @@ public class GenerateReviewFragment extends Fragment { public interface ProgressListener { void showProgressDialog(int msgId); + void showLedgerProgressDialog(int mode); + void dismissProgressDialog(); } @@ -577,4 +616,10 @@ public class GenerateReviewFragment extends Fragment { return openDialog; } + private boolean isKeyValid(String key) { + return (key != null) && (key.length() == 64) + && !key.equals("0000000000000000000000000000000000000000000000000000000000000000") + && !key.toLowerCase().equals("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + // ledger implmenetation returns the spend key as f's + } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java index d63a424..0c2ebb1 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java @@ -16,18 +16,20 @@ package com.m2049r.xmrwallet; -import android.app.Activity; import android.app.AlertDialog; -import android.app.ProgressDialog; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbManager; import android.media.MediaScannerConnection; import android.os.AsyncTask; import android.os.Bundle; -import android.os.Handler; import android.support.annotation.NonNull; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; @@ -46,6 +48,8 @@ import com.m2049r.xmrwallet.dialog.AboutFragment; import com.m2049r.xmrwallet.dialog.CreditsFragment; import com.m2049r.xmrwallet.dialog.HelpFragment; import com.m2049r.xmrwallet.dialog.PrivacyFragment; +import com.m2049r.xmrwallet.ledger.Ledger; +import com.m2049r.xmrwallet.ledger.LedgerProgressDialog; import com.m2049r.xmrwallet.model.NetworkType; import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.model.WalletManager; @@ -72,10 +76,10 @@ import java.util.Locale; import timber.log.Timber; -public class LoginActivity extends SecureActivity +public class LoginActivity extends BaseActivity implements LoginFragment.Listener, GenerateFragment.Listener, GenerateReviewFragment.Listener, GenerateReviewFragment.AcceptListener, - GenerateReviewFragment.ProgressListener, ReceiveFragment.Listener { + ReceiveFragment.Listener { private static final String GENERATE_STACK = "gen"; static final int DAEMON_TIMEOUT = 500; // deamon must respond in 500ms @@ -102,6 +106,11 @@ public class LoginActivity extends SecureActivity toolbar.setTitle(title, subtitle); } + @Override + public boolean hasLedger() { + return Ledger.isConnected(); + } + @Override protected void onCreate(Bundle savedInstanceState) { Timber.d("onCreate()"); @@ -140,6 +149,8 @@ public class LoginActivity extends SecureActivity } else { Timber.i("Waiting for permissions"); } + + processIntent(getIntent()); } boolean checkServiceRunning() { @@ -517,39 +528,12 @@ public class LoginActivity extends SecureActivity super.onPause(); } - ProgressDialog progressDialog = null; - - @Override - public void showProgressDialog(int msgId) { - showProgressDialog(msgId, 0); - } - - private void showProgressDialog(int msgId, long delay) { - dismissProgressDialog(); // just in case - progressDialog = new MyProgressDialog(LoginActivity.this, msgId); - if (delay > 0) { - Handler handler = new Handler(); - handler.postDelayed(new Runnable() { - public void run() { - if (progressDialog != null) progressDialog.show(); - } - }, delay); - } else { - progressDialog.show(); - } - } - - @Override - public void dismissProgressDialog() { - if (progressDialog != null && progressDialog.isShowing()) { - progressDialog.dismiss(); - } - progressDialog = null; - } - @Override protected void onDestroy() { + Timber.d("onDestroy"); dismissProgressDialog(); + unregisterDetachReceiver(); + Ledger.disconnect(); super.onDestroy(); } @@ -562,25 +546,9 @@ public class LoginActivity extends SecureActivity // and show a progress dialog, but only if there isn't one already new AsyncWaitForService().execute(); } + if (!Ledger.isConnected()) attachLedger(); } - private class MyProgressDialog extends ProgressDialog { - Activity activity; - - MyProgressDialog(Activity activity, int msgId) { - super(activity); - this.activity = activity; - setCancelable(false); - setMessage(activity.getString(msgId)); - } - - @Override - public void onBackPressed() { - // prevent back button - } - } - - private class AsyncWaitForService extends AsyncTask { @Override protected void onPreExecute() { @@ -730,7 +698,12 @@ public class LoginActivity extends SecureActivity @Override protected void onPreExecute() { super.onPreExecute(); - showProgressDialog(R.string.generate_wallet_creating); + acquireWakeLock(); + if (walletCreator.isLedger()) { + showLedgerProgressDialog(LedgerProgressDialog.TYPE_RESTORE); + } else { + showProgressDialog(R.string.generate_wallet_creating); + } } @Override @@ -763,6 +736,7 @@ public class LoginActivity extends SecureActivity @Override protected void onPostExecute(Boolean result) { super.onPostExecute(result); + releaseWakeLock(RELEASE_WAKE_LOCK_DELAY); if (isDestroyed()) { return; } @@ -794,12 +768,20 @@ public class LoginActivity extends SecureActivity interface WalletCreator { boolean createWallet(File aFile, String password); + boolean isLedger(); + } @Override public void onGenerate(final String name, final String password) { createWallet(name, password, new WalletCreator() { + @Override + public boolean isLedger() { + return false; + } + + @Override public boolean createWallet(File aFile, String password) { Wallet newWallet = WalletManager.getInstance() .createWallet(aFile, password, MNEMONIC_LANGUAGE); @@ -819,9 +801,40 @@ public class LoginActivity extends SecureActivity final long restoreHeight) { createWallet(name, password, new WalletCreator() { + @Override + public boolean isLedger() { + return false; + } + + @Override public boolean createWallet(File aFile, String password) { - Wallet newWallet = WalletManager.getInstance(). - recoveryWallet(aFile, password, seed, restoreHeight); + Wallet newWallet = WalletManager.getInstance() + .recoveryWallet(aFile, password, seed, restoreHeight); + boolean success = (newWallet.getStatus() == Wallet.Status.Status_Ok); + if (!success) { + Timber.e(newWallet.getErrorString()); + toast(newWallet.getErrorString()); + } + newWallet.close(); + return success; + } + }); + } + + @Override + public void onGenerateLedger(final String name, final String password, final long restoreHeight) { + createWallet(name, password, + new WalletCreator() { + @Override + public boolean isLedger() { + return true; + } + + @Override + public boolean createWallet(File aFile, String password) { + Wallet newWallet = WalletManager.getInstance() + .createWalletFromDevice(aFile, password, + restoreHeight, "Ledger"); boolean success = (newWallet.getStatus() == Wallet.Status.Status_Ok); if (!success) { Timber.e(newWallet.getErrorString()); @@ -839,6 +852,12 @@ public class LoginActivity extends SecureActivity final long restoreHeight) { createWallet(name, password, new WalletCreator() { + @Override + public boolean isLedger() { + return false; + } + + @Override public boolean createWallet(File aFile, String password) { Wallet newWallet = WalletManager.getInstance() .createWalletWithKeys(aFile, password, MNEMONIC_LANGUAGE, restoreHeight, @@ -863,6 +882,15 @@ public class LoginActivity extends SecureActivity }); } + void toast(final int msgId) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(LoginActivity.this, getString(msgId), Toast.LENGTH_LONG).show(); + } + }); + } + @Override public void onAccept(final String name, final String password) { File walletFolder = getStorageRoot(); @@ -870,26 +898,9 @@ public class LoginActivity extends SecureActivity Timber.d("New Wallet %s", walletFile.getAbsolutePath()); walletFile.delete(); // when recovering wallets, the cache seems corrupt - boolean rc = testWallet(walletFile.getAbsolutePath(), password) == Wallet.Status.Status_Ok; - - if (rc) { - popFragmentStack(GENERATE_STACK); - Toast.makeText(LoginActivity.this, - getString(R.string.generate_wallet_created), Toast.LENGTH_SHORT).show(); - } else { - Timber.e("Wallet store failed to %s", walletFile.getAbsolutePath()); - Toast.makeText(LoginActivity.this, getString(R.string.generate_wallet_create_failed), Toast.LENGTH_LONG).show(); - } - } - - Wallet.Status testWallet(String path, String password) { - Timber.d("testing wallet %s", path); - Wallet aWallet = WalletManager.getInstance().openWallet(path, password); - if (aWallet == null) return Wallet.Status.Status_Error; // does this ever happen? - Wallet.Status status = aWallet.getStatus(); - Timber.d("wallet tested %s", aWallet.getStatus()); - aWallet.close(); - return status; + popFragmentStack(GENERATE_STACK); + Toast.makeText(LoginActivity.this, + getString(R.string.generate_wallet_created), Toast.LENGTH_SHORT).show(); } boolean walletExists(File walletFile, boolean any) { @@ -1040,6 +1051,9 @@ public class LoginActivity extends SecureActivity case R.id.action_create_help_seed: HelpFragment.display(getSupportFragmentManager(), R.string.help_create_seed); return true; + case R.id.action_create_help_ledger: + HelpFragment.display(getSupportFragmentManager(), R.string.help_create_ledger); + return true; case R.id.action_details_help: HelpFragment.display(getSupportFragmentManager(), R.string.help_details); return true; @@ -1150,14 +1164,166 @@ public class LoginActivity extends SecureActivity File walletFile = Helper.getWalletFile(this, walletNode.getName()); if (WalletManager.getInstance().walletExists(walletFile)) { WalletManager.getInstance().setDaemon(walletNode); - Helper.promptPassword(LoginActivity.this, walletNode.getName(), false, new Helper.PasswordAction() { - @Override - public void action(String walletName, String password, boolean fingerprintUsed) { - startWallet(walletName, password, fingerprintUsed); - } - }); + Helper.promptPassword(LoginActivity.this, walletNode.getName(), false, + new Helper.PasswordAction() { + @Override + public void action(String walletName, String password, boolean fingerprintUsed) { + String keyPath = new File(Helper.getWalletRoot(LoginActivity.this), + walletName + ".keys").getAbsolutePath(); + // check if we need connected hardware + int hw = WalletManager.getInstance().queryWalletHardware(keyPath, password); + if ((hw == 1) && (!hasLedger())) { + toast(R.string.open_wallet_ledger_missing); + } else { + // hw could be < 0 meaning the password is wrong - this gets dealt with later + startWallet(walletName, password, fingerprintUsed); + } + } + }); } else { // this cannot really happen as we prefilter choices Toast.makeText(this, getString(R.string.bad_wallet), Toast.LENGTH_SHORT).show(); } } + + // USB Stuff - (Ledger) + + private static final String ACTION_USB_PERMISSION = "com.m2049r.xmrwallet.USB_PERMISSION"; + + void attachLedger() { + final UsbManager usbManager = getUsbManager(); + UsbDevice device = Ledger.findDevice(usbManager); + if (device != null) { + if (usbManager.hasPermission(device)) { + connectLedger(usbManager, device); + } else { + registerReceiver(usbPermissionReceiver, new IntentFilter(ACTION_USB_PERMISSION)); + usbManager.requestPermission(device, + PendingIntent.getBroadcast(this, 0, + new Intent(ACTION_USB_PERMISSION), 0)); + } + } else { + Timber.d("no ledger device found"); + } + } + + private final BroadcastReceiver usbPermissionReceiver = new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (ACTION_USB_PERMISSION.equals(action)) { + unregisterReceiver(usbPermissionReceiver); + synchronized (this) { + UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { + if (device != null) { + connectLedger(getUsbManager(), device); + } + } else { + Timber.w("User denied permission for device %s", device.getProductName()); + } + } + } + } + }; + + private void connectLedger(UsbManager usbManager, final UsbDevice usbDevice) { + try { + Ledger.connect(usbManager, usbDevice); + registerDetachReceiver(); + onLedgerAction(); + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(LoginActivity.this, + getString(R.string.toast_ledger_attached, usbDevice.getProductName()), + Toast.LENGTH_SHORT) + .show(); + } + }); + } catch (IOException ex) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(LoginActivity.this, + getString(R.string.open_wallet_ledger_missing), + Toast.LENGTH_SHORT) + .show(); + } + }); + } + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + processIntent(intent); + } + + private void processIntent(Intent intent) { + String action = intent.getAction(); + if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) { + synchronized (this) { + final UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + if (device != null) { + final UsbManager usbManager = getUsbManager(); + if (usbManager.hasPermission(device)) { + Timber.d("Ledger attached by intent"); + connectLedger(usbManager, device); + } + } + } + } + } + + BroadcastReceiver detachReceiver; + + private void unregisterDetachReceiver() { + if (detachReceiver != null) unregisterReceiver(detachReceiver); + } + + private void registerDetachReceiver() { + detachReceiver = new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) { + unregisterDetachReceiver(); + final UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + Timber.i("Ledger detached!"); + if (device != null) + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(LoginActivity.this, + getString(R.string.toast_ledger_detached, device.getProductName()), + Toast.LENGTH_SHORT) + .show(); + } + }); + Ledger.disconnect(); + onLedgerAction(); + } + } + }; + + registerReceiver(detachReceiver, new IntentFilter(UsbManager.ACTION_USB_DEVICE_DETACHED)); + } + + public void onLedgerAction() { + Fragment f = getSupportFragmentManager().findFragmentById(R.id.fragment_container); + if (f instanceof GenerateFragment) { + onBackPressed(); + } else if (f instanceof LoginFragment) { + if (((LoginFragment) f).isFabOpen()) { + ((LoginFragment) f).animateFAB(); + } + } + } + + // get UsbManager or die trying + @NonNull + private UsbManager getUsbManager() { + final UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); + if (usbManager == null) { + throw new IllegalStateException("no USB_SERVICE"); + } + return usbManager; + } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java b/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java index ce97e3e..fe67dd6 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java @@ -103,6 +103,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter void setNetworkType(NetworkType networkType); + boolean hasLedger(); } @Override @@ -145,11 +146,13 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter fabView = (FloatingActionButton) view.findViewById(R.id.fabView); fabKey = (FloatingActionButton) view.findViewById(R.id.fabKey); fabSeed = (FloatingActionButton) view.findViewById(R.id.fabSeed); + fabLedger = (FloatingActionButton) view.findViewById(R.id.fabLedger); fabNewL = (RelativeLayout) view.findViewById(R.id.fabNewL); fabViewL = (RelativeLayout) view.findViewById(R.id.fabViewL); fabKeyL = (RelativeLayout) view.findViewById(R.id.fabKeyL); fabSeedL = (RelativeLayout) view.findViewById(R.id.fabSeedL); + fabLedgerL = (RelativeLayout) view.findViewById(R.id.fabLedgerL); fab_pulse = AnimationUtils.loadAnimation(getContext(), R.anim.fab_pulse); fab_open_screen = AnimationUtils.loadAnimation(getContext(), R.anim.fab_open_screen); @@ -163,6 +166,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter fabView.setOnClickListener(this); fabKey.setOnClickListener(this); fabSeed.setOnClickListener(this); + fabLedger.setOnClickListener(this); fabScreen.setOnClickListener(this); RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.list); @@ -173,7 +177,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter etDummy = (EditText) view.findViewById(R.id.etDummy); ViewGroup llNotice = (ViewGroup) view.findViewById(R.id.llNotice); - Notice.showAll(llNotice,".*_login"); + Notice.showAll(llNotice, ".*_login"); etDaemonAddress = (DropDownEditText) view.findViewById(R.id.etDaemonAddress); nodeAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_dropdown_item_1line); @@ -426,9 +430,9 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter } private boolean isFabOpen = false; - private FloatingActionButton fab, fabNew, fabView, fabKey, fabSeed; + private FloatingActionButton fab, fabNew, fabView, fabKey, fabSeed, fabLedger; private FrameLayout fabScreen; - private RelativeLayout fabNewL, fabViewL, fabKeyL, fabSeedL; + private RelativeLayout fabNewL, fabViewL, fabKeyL, fabSeedL, fabLedgerL; private Animation fab_open, fab_close, rotate_forward, rotate_backward, fab_open_screen, fab_close_screen; private Animation fab_pulse; @@ -437,32 +441,53 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter } public void animateFAB() { - if (isFabOpen) { - fabScreen.setVisibility(View.INVISIBLE); + if (isFabOpen) { // close the fab fabScreen.setClickable(false); fabScreen.startAnimation(fab_close_screen); fab.startAnimation(rotate_backward); - fabNewL.startAnimation(fab_close); - fabNew.setClickable(false); - fabViewL.startAnimation(fab_close); - fabView.setClickable(false); - fabKeyL.startAnimation(fab_close); - fabKey.setClickable(false); - fabSeedL.startAnimation(fab_close); - fabSeed.setClickable(false); + if (fabLedgerL.getVisibility() == View.VISIBLE) { + fabLedgerL.startAnimation(fab_close); + fabLedger.setClickable(false); + } else { + fabNewL.startAnimation(fab_close); + fabNew.setClickable(false); + fabViewL.startAnimation(fab_close); + fabView.setClickable(false); + fabKeyL.startAnimation(fab_close); + fabKey.setClickable(false); + fabSeedL.startAnimation(fab_close); + fabSeed.setClickable(false); + } isFabOpen = false; - } else { + } else { // open the fab fabScreen.setClickable(true); fabScreen.startAnimation(fab_open_screen); fab.startAnimation(rotate_forward); - fabNewL.startAnimation(fab_open); - fabNew.setClickable(true); - fabViewL.startAnimation(fab_open); - fabView.setClickable(true); - fabKeyL.startAnimation(fab_open); - fabKey.setClickable(true); - fabSeedL.startAnimation(fab_open); - fabSeed.setClickable(true); + if (activityCallback.hasLedger()) { + fabLedgerL.setVisibility(View.VISIBLE); + fabNewL.setVisibility(View.GONE); + fabViewL.setVisibility(View.GONE); + fabKeyL.setVisibility(View.GONE); + fabSeedL.setVisibility(View.GONE); + + fabLedgerL.startAnimation(fab_open); + fabLedger.setClickable(true); + } else { + fabLedgerL.setVisibility(View.GONE); + fabNewL.setVisibility(View.VISIBLE); + fabViewL.setVisibility(View.VISIBLE); + fabKeyL.setVisibility(View.VISIBLE); + fabSeedL.setVisibility(View.VISIBLE); + + fabNewL.startAnimation(fab_open); + fabNew.setClickable(true); + fabViewL.startAnimation(fab_open); + fabView.setClickable(true); + fabKeyL.startAnimation(fab_open); + fabKey.setClickable(true); + fabSeedL.startAnimation(fab_open); + fabSeed.setClickable(true); + } isFabOpen = true; } } @@ -470,6 +495,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter @Override public void onClick(View v) { int id = v.getId(); + Timber.d("onClick %d/%d", id, R.id.fabLedger); switch (id) { case R.id.fab: animateFAB(); @@ -491,6 +517,11 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter animateFAB(); activityCallback.onAddWallet(GenerateFragment.TYPE_SEED); break; + case R.id.fabLedger: + Timber.d("FAB_LEDGER"); + animateFAB(); + activityCallback.onAddWallet(GenerateFragment.TYPE_LEDGER); + break; case R.id.fabScreen: animateFAB(); break; diff --git a/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java b/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java index 3e92f63..8b19dd8 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java @@ -47,6 +47,7 @@ import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.QRCodeWriter; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import com.m2049r.xmrwallet.data.BarcodeData; +import com.m2049r.xmrwallet.ledger.LedgerProgressDialog; import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.model.WalletManager; import com.m2049r.xmrwallet.util.Helper; @@ -62,7 +63,6 @@ import timber.log.Timber; public class ReceiveFragment extends Fragment { private ProgressBar pbProgress; - private View llAddress; private TextView tvAddressLabel; private TextView tvAddress; private TextInputLayout etPaymentId; @@ -93,7 +93,6 @@ public class ReceiveFragment extends Fragment { View view = inflater.inflate(R.layout.fragment_receive, container, false); pbProgress = (ProgressBar) view.findViewById(R.id.pbProgress); - llAddress = view.findViewById(R.id.llAddress); tvAddressLabel = (TextView) view.findViewById(R.id.tvAddressLabel); tvAddress = (TextView) view.findViewById(R.id.tvAddress); etPaymentId = (TextInputLayout) view.findViewById(R.id.etPaymentId); @@ -177,23 +176,9 @@ public class ReceiveFragment extends Fragment { enableSubaddressButton(false); enableCopyAddress(false); - final Runnable resetSize = new Runnable() { - public void run() { - tvAddress.animate().setDuration(125).scaleX(1).scaleY(1).start(); - } - }; - final Runnable newAddress = new Runnable() { public void run() { - tvAddress.setText(wallet.getNewSubaddress()); - tvAddressLabel.setText(getString(R.string.generate_address_label_sub, - wallet.getNumSubaddresses() - 1)); - storeWallet(); - generateQr(); - enableCopyAddress(true); - tvAddress.animate().alpha(1).setDuration(125) - .scaleX(1.2f).scaleY(1.2f) - .withEndAction(resetSize).start(); + getNewSubaddress(); } }; @@ -315,18 +300,39 @@ public class ReceiveFragment extends Fragment { } private void loadAndShow(String walletPath, String password) { - new AsyncShow().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR, - walletPath, password); + new AsyncShow(walletPath, password).executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR); } - private class AsyncShow extends AsyncTask { - String password; + GenerateReviewFragment.ProgressListener progressCallback = null; + + private class AsyncShow extends AsyncTask { + final private String walletPath; + final private String password; + + + AsyncShow(String walletPath, String passsword) { + super(); + this.walletPath = walletPath; + this.password = passsword; + } + + boolean dialogOpened = false; @Override - protected Boolean doInBackground(String... params) { - if (params.length != 2) return false; - String walletPath = params[0]; - password = params[1]; + protected void onPreExecute() { + super.onPreExecute(); + showProgress(); + if ((walletPath != null) + && (WalletManager.getInstance().queryWalletHardware(walletPath + ".keys", password) == 1) + && (progressCallback != null)) { + progressCallback.showLedgerProgressDialog(LedgerProgressDialog.TYPE_RESTORE); + dialogOpened = true; + } + } + + @Override + protected Boolean doInBackground(Void... params) { + if (params.length != 0) return false; wallet = WalletManager.getInstance().openWallet(walletPath, password); isMyWallet = true; return true; @@ -335,6 +341,8 @@ public class ReceiveFragment extends Fragment { @Override protected void onPostExecute(Boolean result) { super.onPostExecute(result); + if (dialogOpened) + progressCallback.dismissProgressDialog(); if (!isAdded()) return; // never mind if (result) { show(); @@ -495,6 +503,10 @@ public class ReceiveFragment extends Fragment { throw new ClassCastException(context.toString() + " must implement Listener"); } + if (context instanceof GenerateReviewFragment.ProgressListener) { + this.progressCallback = (GenerateReviewFragment.ProgressListener) context; + } + } @Override @@ -513,4 +525,51 @@ public class ReceiveFragment extends Fragment { } super.onDetach(); } + + private void getNewSubaddress() { + new AsyncSubaddress().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR); + } + + private class AsyncSubaddress extends AsyncTask { + private String newSubaddress; + + boolean dialogOpened = false; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + if (wallet.isKeyOnDevice() && (progressCallback != null)) { + progressCallback.showLedgerProgressDialog(LedgerProgressDialog.TYPE_SUBADDRESS); + dialogOpened = true; + } + } + + @Override + protected Boolean doInBackground(Void... params) { + if (params.length != 0) return false; + newSubaddress = wallet.getNewSubaddress(); + storeWallet(); + return true; + } + + @Override + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + if (dialogOpened) + progressCallback.dismissProgressDialog(); + tvAddress.setText(newSubaddress); + tvAddressLabel.setText(getString(R.string.generate_address_label_sub, + wallet.getNumSubaddresses() - 1)); + generateQr(); + enableCopyAddress(true); + final Runnable resetSize = new Runnable() { + public void run() { + tvAddress.animate().setDuration(125).scaleX(1).scaleY(1).start(); + } + }; + tvAddress.animate().alpha(1).setDuration(125) + .scaleX(1.2f).scaleY(1.2f) + .withEndAction(resetSize).start(); + } + } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java index 52de0c9..e6d4fb0 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java @@ -24,6 +24,7 @@ import android.content.Intent; import android.content.ServiceConnection; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.os.AsyncTask; import android.os.Bundle; import android.os.IBinder; import android.os.PowerManager; @@ -51,12 +52,14 @@ import com.m2049r.xmrwallet.dialog.CreditsFragment; import com.m2049r.xmrwallet.dialog.HelpFragment; import com.m2049r.xmrwallet.fragment.send.SendAddressWizardFragment; import com.m2049r.xmrwallet.fragment.send.SendFragment; +import com.m2049r.xmrwallet.ledger.LedgerProgressDialog; import com.m2049r.xmrwallet.model.PendingTransaction; import com.m2049r.xmrwallet.model.TransactionInfo; import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.model.WalletManager; import com.m2049r.xmrwallet.service.WalletService; import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.MoneroThreadPoolExecutor; import com.m2049r.xmrwallet.util.UserNotes; import com.m2049r.xmrwallet.widget.Toolbar; @@ -65,7 +68,7 @@ import java.util.List; import timber.log.Timber; -public class WalletActivity extends SecureActivity implements WalletFragment.Listener, +public class WalletActivity extends BaseActivity implements WalletFragment.Listener, WalletService.Observer, SendFragment.Listener, TxFragment.Listener, GenerateReviewFragment.ListenerWithWallet, GenerateReviewFragment.Listener, @@ -397,28 +400,6 @@ public class WalletActivity extends SecureActivity implements WalletFragment.Lis Timber.d("onResume()"); } - private PowerManager.WakeLock wl = null; - - void acquireWakeLock() { - if ((wl != null) && wl.isHeld()) return; - PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - this.wl = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, getString(R.string.app_name)); - try { - wl.acquire(); - Timber.d("WakeLock acquired"); - } catch (SecurityException ex) { - Timber.w("WakeLock NOT acquired: %s", ex.getLocalizedMessage()); - wl = null; - } - } - - public void releaseWakeLock() { - if ((wl == null) || !wl.isHeld()) return; - wl.release(); - wl = null; - Timber.d("WakeLock released"); - } - public void saveWallet() { if (mIsBound) { // no point in talking to unbound service Intent intent = new Intent(getApplicationContext(), WalletService.class); @@ -503,7 +484,7 @@ public class WalletActivity extends SecureActivity implements WalletFragment.Lis getSupportFragmentManager().findFragmentByTag(WalletFragment.class.getName()); if (wallet.isSynchronized()) { Timber.d("onRefreshed() synced"); - releaseWakeLock(); // the idea is to stay awake until synced + releaseWakeLock(RELEASE_WAKE_LOCK_DELAY); // the idea is to stay awake until synced if (!synced) { // first sync onProgress(-1); saveWallet(); // save on first sync @@ -544,10 +525,21 @@ public class WalletActivity extends SecureActivity implements WalletFragment.Lis boolean haveWallet = false; + @Override + public void onWalletOpen(final int hardware) { + if (hardware > 0) + runOnUiThread(new Runnable() { + public void run() { + showLedgerProgressDialog(LedgerProgressDialog.TYPE_RESTORE); + } + }); + } + @Override public void onWalletStarted(final boolean success) { runOnUiThread(new Runnable() { public void run() { + dismissProgressDialog(); if (!success) { Toast.makeText(WalletActivity.this, getString(R.string.status_wallet_connect_failed), Toast.LENGTH_LONG).show(); } @@ -578,6 +570,7 @@ public class WalletActivity extends SecureActivity implements WalletFragment.Lis getSupportFragmentManager().findFragmentById(R.id.fragment_container); runOnUiThread(new Runnable() { public void run() { + dismissProgressDialog(); PendingTransaction.Status status = pendingTransaction.getStatus(); if (status != PendingTransaction.Status.Status_Ok) { String errorText = pendingTransaction.getErrorString(); @@ -733,6 +726,8 @@ public class WalletActivity extends SecureActivity implements WalletFragment.Lis intent.putExtra(WalletService.REQUEST_CMD_TX_TAG, tag); startService(intent); Timber.d("CREATE TX request sent"); + if (getWallet().isKeyOnDevice()) + showLedgerProgressDialog(LedgerProgressDialog.TYPE_SEND); } else { Timber.e("Service not bound"); } @@ -1049,12 +1044,7 @@ public class WalletActivity extends SecureActivity implements WalletFragment.Lis final int id = item.getItemId(); switch (id) { case R.id.account_new: - getWallet().addAccount(); - int newIdx = getWallet().getNumAccounts() - 1; - getWallet().setAccountIndex(newIdx); - Toast.makeText(this, - getString(R.string.accounts_new, newIdx), - Toast.LENGTH_SHORT).show(); + addAccount(); break; default: Timber.d("NavigationDrawer ID=%d", id); @@ -1063,9 +1053,49 @@ public class WalletActivity extends SecureActivity implements WalletFragment.Lis Timber.d("found @%d", accountIdx); getWallet().setAccountIndex(accountIdx); } + forceUpdate(); + drawer.closeDrawer(GravityCompat.START); } - forceUpdate(); - drawer.closeDrawer(GravityCompat.START); return true; } + + private void addAccount() { + new AsyncAddAccount().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR); + } + + private class AsyncAddAccount extends AsyncTask { + boolean dialogOpened = false; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + if (getWallet().isKeyOnDevice()) { + showLedgerProgressDialog(LedgerProgressDialog.TYPE_ACCOUNT); + dialogOpened = true; + } else { + showProgressDialog(R.string.accounts_progress_new); + dialogOpened = true; + } + } + + @Override + protected Boolean doInBackground(Void... params) { + if (params.length != 0) return false; + getWallet().addAccount(); + getWallet().setAccountIndex(getWallet().getNumAccounts() - 1); + return true; + } + + @Override + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + forceUpdate(); + drawer.closeDrawer(GravityCompat.START); + if (dialogOpened) + dismissProgressDialog(); + Toast.makeText(WalletActivity.this, + getString(R.string.accounts_new, getWallet().getNumAccounts() - 1), + Toast.LENGTH_SHORT).show(); + } + } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/dialog/ProgressDialog.java b/app/src/main/java/com/m2049r/xmrwallet/dialog/ProgressDialog.java new file mode 100644 index 0000000..aa73f74 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/dialog/ProgressDialog.java @@ -0,0 +1,130 @@ +package com.m2049r.xmrwallet.dialog; + +/* + * Copyright (C) 2007 The Android Open Source Project + * Copyright (C) 2018 m2049r + * + * 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. + */ + +import android.app.AlertDialog; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.m2049r.xmrwallet.R; + +import java.util.Locale; + +import timber.log.Timber; + +public class ProgressDialog extends AlertDialog { + + private ProgressBar pbBar; + + private TextView tvMessage; + + private TextView tvProgress; + + private View rlProgressBar, pbCircle; + + static private final String PROGRESS_FORMAT = "%1d/%2d"; + + private CharSequence message; + private int maxValue, progressValue; + private boolean indeterminate = true; + + public ProgressDialog(Context context) { + super(context); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + final View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_ledger_progress, null); + pbCircle = view.findViewById(R.id.pbCircle); + tvMessage = (TextView) view.findViewById(R.id.tvMessage); + rlProgressBar = view.findViewById(R.id.rlProgressBar); + pbBar = (ProgressBar) view.findViewById(R.id.pbBar); + tvProgress = (TextView) view.findViewById(R.id.tvProgress); + setView(view); + //setTitle("blabla"); + //super.setMessage("bubbu"); +// view.invalidate(); + setIndeterminate(indeterminate); + if (maxValue > 0) { + setMax(maxValue); + } + if (progressValue > 0) { + setProgress(progressValue); + } + if (message != null) { + Timber.d("msg=%s", message); + setMessage(message); + } + + super.onCreate(savedInstanceState); + } + + public void setProgress(int value, int max) { + progressValue = value; + maxValue = max; + if (pbBar != null) { + pbBar.setProgress(value); + pbBar.setMax(max); + tvProgress.setText(String.format(Locale.getDefault(), PROGRESS_FORMAT, value, maxValue)); + } + } + + public void setProgress(int value) { + progressValue = value; + if (pbBar != null) { + pbBar.setProgress(value); + tvProgress.setText(String.format(Locale.getDefault(), PROGRESS_FORMAT, value, maxValue)); + } + } + + public void setMax(int max) { + maxValue = max; + if (pbBar != null) { + pbBar.setMax(max); + } + } + + public void setIndeterminate(boolean indeterminate) { + if (this.indeterminate != indeterminate) { + if (rlProgressBar != null) { + if (indeterminate) { + pbCircle.setVisibility(View.VISIBLE); + rlProgressBar.setVisibility(View.GONE); + } else { + pbCircle.setVisibility(View.GONE); + rlProgressBar.setVisibility(View.VISIBLE); + } + } + this.indeterminate = indeterminate; + } + } + + @Override + public void setMessage(CharSequence message) { + this.message = message; + if (tvMessage != null) { + tvMessage.setText(message); + } + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/ledger/Instruction.java b/app/src/main/java/com/m2049r/xmrwallet/ledger/Instruction.java new file mode 100644 index 0000000..512fe77 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/ledger/Instruction.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2018 m2049r + * + * 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.ledger; + +public enum Instruction { + INS_NONE(0x00), + INS_RESET(0x02), + INS_GET_KEY(0x20), + INS_PUT_KEY(0x22), + INS_GET_CHACHA8_PREKEY(0x24), + INS_VERIFY_KEY(0x26), + + INS_SECRET_KEY_TO_PUBLIC_KEY(0x30), + INS_GEN_KEY_DERIVATION(0x32), + INS_DERIVATION_TO_SCALAR(0x34), + INS_DERIVE_PUBLIC_KEY(0x36), + INS_DERIVE_SECRET_KEY(0x38), + INS_GEN_KEY_IMAGE(0x3A), + INS_SECRET_KEY_ADD(0x3C), + INS_SECRET_KEY_SUB(0x3E), + INS_GENERATE_KEYPAIR(0x40), + INS_SECRET_SCAL_MUL_KEY(0x42), + INS_SECRET_SCAL_MUL_BASE(0x44), + + INS_DERIVE_SUBADDRESS_PUBLIC_KEY(0x46), + INS_GET_SUBADDRESS(0x48), + INS_GET_SUBADDRESS_SPEND_PUBLIC_KEY(0x4A), + INS_GET_SUBADDRESS_SECRET_KEY(0x4C), + + INS_OPEN_TX(0x70), + INS_SET_SIGNATURE_MODE(0x72), + INS_GET_ADDITIONAL_KEY(0x74), + INS_STEALTH(0x76), + INS_BLIND(0x78), + INS_UNBLIND(0x7A), + INS_VALIDATE(0x7C), + INS_MLSAG(0x7E), + INS_CLOSE_TX(0x80), + + INS_GET_RESPONSE(0xc0), + + INS_UNDEFINED(0xff);; + + public static Instruction fromByte(byte n) { + switch (n & 0xFF) { + case 0x00: + return INS_NONE; + case 0x02: + return INS_RESET; + + case 0x20: + return INS_GET_KEY; + case 0x22: + return INS_PUT_KEY; + case 0x24: + return INS_GET_CHACHA8_PREKEY; + case 0x26: + return INS_VERIFY_KEY; + + case 0x30: + return INS_SECRET_KEY_TO_PUBLIC_KEY; + case 0x32: + return INS_GEN_KEY_DERIVATION; + case 0x34: + return INS_DERIVATION_TO_SCALAR; + case 0x36: + return INS_DERIVE_PUBLIC_KEY; + case 0x38: + return INS_DERIVE_SECRET_KEY; + case 0x3A: + return INS_GEN_KEY_IMAGE; + case 0x3C: + return INS_SECRET_KEY_ADD; + case 0x3E: + return INS_SECRET_KEY_SUB; + case 0x40: + return INS_GENERATE_KEYPAIR; + case 0x42: + return INS_SECRET_SCAL_MUL_KEY; + case 0x44: + return INS_SECRET_SCAL_MUL_BASE; + + case 0x46: + return INS_DERIVE_SUBADDRESS_PUBLIC_KEY; + case 0x48: + return INS_GET_SUBADDRESS; + case 0x4A: + return INS_GET_SUBADDRESS_SPEND_PUBLIC_KEY; + case 0x4C: + return INS_GET_SUBADDRESS_SECRET_KEY; + + case 0x70: + return INS_OPEN_TX; + case 0x72: + return INS_SET_SIGNATURE_MODE; + case 0x74: + return INS_GET_ADDITIONAL_KEY; + case 0x76: + return INS_STEALTH; + case 0x78: + return INS_BLIND; + case 0x7A: + return INS_UNBLIND; + case 0x7C: + return INS_VALIDATE; + case 0x7E: + return INS_MLSAG; + case 0x80: + return INS_CLOSE_TX; + + case 0xc0: + return INS_GET_RESPONSE; + + default: + return INS_UNDEFINED; + } + } + + public int getValue() { + return value; + } + + private int value; + + Instruction(int value) { + this.value = value; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/ledger/Ledger.java b/app/src/main/java/com/m2049r/xmrwallet/ledger/Ledger.java new file mode 100644 index 0000000..e761b08 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/ledger/Ledger.java @@ -0,0 +1,272 @@ +/* + ******************************************************************************* + * BTChip Bitcoin Hardware Wallet Java API + * (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn + * (c) m2049r + * + * 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.ledger; + +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbManager; + +import com.btchip.BTChipException; +import com.btchip.comm.BTChipTransport; +import com.btchip.comm.android.BTChipTransportAndroidHID; +import com.m2049r.xmrwallet.BuildConfig; +import com.m2049r.xmrwallet.util.Helper; + +import java.io.IOException; + +import timber.log.Timber; + +public class Ledger { + // lookahead parameters as suggest on + // https://monero.stackexchange.com/a/9902/8977 (Step 8) + // by dEBRUYNE + static public final int LOOKAHEAD_ACCOUNTS = 3; + static public final int LOOKAHEAD_SUBADDRESSES = 100; + static public final String SUBADDRESS_LOOKAHEAD = LOOKAHEAD_ACCOUNTS + ":" + LOOKAHEAD_SUBADDRESSES; + + public static final int SW_OK = 0x9000; + public static final int SW_INS_NOT_SUPPORTED = 0x6D00; + public static final int OK[] = {SW_OK}; + + public static UsbDevice findDevice(UsbManager usbManager) { + return BTChipTransportAndroidHID.getDevice(usbManager); + } + + static private Ledger Instance = null; + + static public String connect(UsbManager usbManager, UsbDevice usbDevice) throws IOException { + if (Instance != null) { + disconnect(); + } + Instance = new Ledger(usbManager, usbDevice); + return Name(); + } + + static public void disconnect() { + // this is not synchronized so as to close immediately + if (Instance != null) { + Instance.close(); + Instance = null; + } + } + + static public boolean isConnected() { + //TODO synchronize with connect/disconnect? + return Instance != null; + } + + static public String Name() { + if (Instance != null) { + return Instance.name; + } else { + return null; + } + } + + static public byte[] Exchange(byte[] apdu) { + if (Instance != null) { + Timber.d("INS: %s", Instruction.fromByte(apdu[1])); + return Instance.exchangeRaw(apdu); + } else { + return null; + } + } + + final private BTChipTransport transport; + final private String name; + private int lastSW = 0; + + private Ledger(UsbManager usbManager, UsbDevice usbDevice) throws IOException { + final BTChipTransport transport = BTChipTransportAndroidHID.open(usbManager, usbDevice); + Timber.d("transport opened = %s", transport.toString()); + transport.setDebug(BuildConfig.DEBUG); + this.transport = transport; + this.name = usbDevice.getManufacturerName() + " " + usbDevice.getProductName(); + initKey(); + } + + synchronized private void close() { + initKey(); // don't leak key after we disconnect + transport.close(); + Timber.d("transport closed"); + lastSW = 0; + } + + synchronized private byte[] exchangeRaw(byte[] apdu) { + if (transport == null) + throw new IllegalStateException("No transport (probably closed previously)"); + Timber.i("exchangeRaw %02x", apdu[1]); + Instruction ins = Instruction.fromByte(apdu[1]); + if (listener != null) listener.onInstructionSend(ins, apdu); + sniffOut(ins, apdu); + byte[] data = transport.exchange(apdu); + if (listener != null) listener.onInstructionReceive(ins, data); + sniffIn(data); + return data; + + } + + private byte[] exchange(byte[] apdu) throws BTChipException { + byte[] response = exchangeRaw(apdu); + if (response.length < 2) { + throw new BTChipException("Truncated response"); + } + lastSW = ((int) (response[response.length - 2] & 0xff) << 8) | + (int) (response[response.length - 1] & 0xff); + byte[] result = new byte[response.length - 2]; + System.arraycopy(response, 0, result, 0, response.length - 2); + return result; + } + + private byte[] exchangeCheck(byte[] apdu, int acceptedSW[]) throws BTChipException { + byte[] response = exchange(apdu); + if (acceptedSW == null) { + return response; + } + for (int SW : acceptedSW) { + if (lastSW == SW) { + return response; + } + } + throw new BTChipException("Invalid status", lastSW); + } + + private byte[] exchangeApdu(byte cla, byte ins, byte p1, byte p2, byte[] data, int acceptedSW[]) throws BTChipException { + byte[] apdu = new byte[data.length + 5]; + apdu[0] = cla; + apdu[1] = ins; + apdu[2] = p1; + apdu[3] = p2; + apdu[4] = (byte) (data.length); + System.arraycopy(data, 0, apdu, 5, data.length); + return exchangeCheck(apdu, acceptedSW); + } + + private byte[] exchangeApdu(byte cla, byte ins, byte p1, byte p2, int length, int acceptedSW[]) throws BTChipException { + byte[] apdu = new byte[5]; + apdu[0] = cla; + apdu[1] = ins; + apdu[2] = p1; + apdu[3] = p2; + apdu[4] = (byte) (length); + return exchangeCheck(apdu, acceptedSW); + } + + private byte[] exchangeApduSplit(byte cla, byte ins, byte p1, byte p2, byte[] data, int acceptedSW[]) throws BTChipException { + int offset = 0; + byte[] result = null; + while (offset < data.length) { + int blockLength = ((data.length - offset) > 255 ? 255 : data.length - offset); + byte[] apdu = new byte[blockLength + 5]; + apdu[0] = cla; + apdu[1] = ins; + apdu[2] = p1; + apdu[3] = p2; + apdu[4] = (byte) (blockLength); + System.arraycopy(data, offset, apdu, 5, blockLength); + result = exchangeCheck(apdu, acceptedSW); + offset += blockLength; + } + return result; + } + + private byte[] exchangeApduSplit2(byte cla, byte ins, byte p1, byte p2, byte[] data, byte[] data2, int acceptedSW[]) throws BTChipException { + int offset = 0; + byte[] result = null; + int maxBlockSize = 255 - data2.length; + while (offset < data.length) { + int blockLength = ((data.length - offset) > maxBlockSize ? maxBlockSize : data.length - offset); + boolean lastBlock = ((offset + blockLength) == data.length); + byte[] apdu = new byte[blockLength + 5 + (lastBlock ? data2.length : 0)]; + apdu[0] = cla; + apdu[1] = ins; + apdu[2] = p1; + apdu[3] = p2; + apdu[4] = (byte) (blockLength + (lastBlock ? data2.length : 0)); + System.arraycopy(data, offset, apdu, 5, blockLength); + if (lastBlock) { + System.arraycopy(data2, 0, apdu, 5 + blockLength, data2.length); + } + result = exchangeCheck(apdu, acceptedSW); + offset += blockLength; + } + return result; + } + + public interface Listener { + void onInstructionSend(Instruction ins, byte[] apdu); + + void onInstructionReceive(Instruction ins, byte[] data); + } + + Listener listener; + + static public void setListener(Listener listener) { + if (Instance != null) { + Instance.listener = listener; + } + } + + static public void unsetListener(Listener listener) { + if ((Instance != null) && (Instance.listener == listener)) + Instance.listener = null; + } + + // very stupid hack to extract the view key + // without messing around with monero core code + // NB: as all the ledger comm can be sniffed off the USB cable - there is no security issue here + private boolean snoopKey = false; + private byte[] key; + + private void initKey() { + key = Helper.hexToBytes("0000000000000000000000000000000000000000000000000000000000000000"); + } + + static public String Key() { + if (Instance != null) { + return Helper.bytesToHex(Instance.key).toLowerCase(); + } else { + return null; + } + } + + private void sniffOut(Instruction ins, byte[] apdu) { + if (ins == Instruction.INS_GET_KEY) { + snoopKey = (apdu[2] == 2); + } + + } + + private void sniffIn(byte[] data) { + // stupid hack to extract the view key + // without messing around with monero core code + if (snoopKey) { + if (data.length == 34) { // 32 key + result code 9000 + long sw = ((data[data.length - 2] & 0xff) << 8) | + (data[data.length - 1] & 0xff); + Timber.e("WS %d", sw); + if (sw == SW_OK) { + System.arraycopy(data, 0, key, 0, 32); + } + } + snoopKey = false; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/ledger/LedgerProgressDialog.java b/app/src/main/java/com/m2049r/xmrwallet/ledger/LedgerProgressDialog.java new file mode 100644 index 0000000..6730d3e --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/ledger/LedgerProgressDialog.java @@ -0,0 +1,162 @@ +package com.m2049r.xmrwallet.ledger; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; + +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.dialog.ProgressDialog; +import com.m2049r.xmrwallet.model.WalletManager; + +import timber.log.Timber; + +public class LedgerProgressDialog extends ProgressDialog implements Ledger.Listener { + + static public final int TYPE_DEBUG = 0; + static public final int TYPE_RESTORE = 1; + static public final int TYPE_SUBADDRESS = 2; + static public final int TYPE_ACCOUNT = 3; + static public final int TYPE_SEND = 4; + + private final int type; + private Handler uiHandler = new Handler(Looper.getMainLooper()); + + public LedgerProgressDialog(Context context, int type) { + super(context); + this.type = type; + setCancelable(false); + if (type == TYPE_SEND) + setMessage(context.getString(R.string.info_prepare_tx)); + else + setMessage(context.getString(R.string.progress_ledger_progress)); + } + + @Override + public void onBackPressed() { + // prevent back button + } + + private int firstSubaddress = Integer.MAX_VALUE; + + private boolean validate = false; + private boolean validated = false; + + @Override + public void onInstructionSend(final Instruction ins, final byte[] apdu) { + Timber.d("LedgerProgressDialog SEND %s", ins); + uiHandler.post(new Runnable() { + @Override + public void run() { + if (type > TYPE_DEBUG) { + validate = false; + switch (ins) { + case INS_RESET: // ledger may ask for confirmation - maybe a bug? + case INS_GET_KEY: // ledger asks for confirmation to send keys + setIndeterminate(true); + setMessage(getContext().getString(R.string.progress_ledger_confirm)); + break; + case INS_GET_SUBADDRESS_SPEND_PUBLIC_KEY: // lookahead + //00 4a 00 00 09 00 01000000 30000000 + // 0 1 2 3 4 5 6 7 8 9 a b c d + int account = bytesToInteger(apdu, 6); + int subaddress = bytesToInteger(apdu, 10); + Timber.d("fetching subaddress (%d, %d)", account, subaddress); + switch (type) { + case TYPE_RESTORE: + setProgress(account * Ledger.LOOKAHEAD_SUBADDRESSES + subaddress + 1, + Ledger.LOOKAHEAD_ACCOUNTS * Ledger.LOOKAHEAD_SUBADDRESSES); + setIndeterminate(false); + break; + case TYPE_ACCOUNT: + final int requestedSubaddress = account * Ledger.LOOKAHEAD_SUBADDRESSES + subaddress; + if (firstSubaddress > requestedSubaddress) { + firstSubaddress = requestedSubaddress; + } + setProgress(requestedSubaddress - firstSubaddress + 1, + Ledger.LOOKAHEAD_ACCOUNTS * Ledger.LOOKAHEAD_SUBADDRESSES); + setIndeterminate(false); + break; + case TYPE_SUBADDRESS: + if (firstSubaddress > subaddress) { + firstSubaddress = subaddress; + } + setProgress(subaddress - firstSubaddress + 1, Ledger.LOOKAHEAD_SUBADDRESSES); + setIndeterminate(false); + break; + default: + setIndeterminate(true); + break; + } + setMessage(getContext().getString(R.string.progress_ledger_lookahead)); + break; + case INS_VERIFY_KEY: + setIndeterminate(true); + setMessage(getContext().getString(R.string.progress_ledger_verify)); + break; + case INS_OPEN_TX: + setIndeterminate(true); + setMessage(getContext().getString(R.string.progress_ledger_opentx)); + break; + case INS_MLSAG: + if (validated) { + setIndeterminate(true); + setMessage(getContext().getString(R.string.progress_ledger_mlsag)); + } + break; + case INS_VALIDATE: + if ((apdu[2] != 1) || (apdu[3] != 1)) break; + validate = true; + uiHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (validate) { + setIndeterminate(true); + setMessage(getContext().getString(R.string.progress_ledger_confirm)); + validated = true; + } + } + }, 250); + break; + default: + // ignore others and maintain state + } + } else { + setMessage(ins.name()); + } + } + }); + } + + @Override + public void onInstructionReceive(final Instruction ins, final byte[] data) { + Timber.d("LedgerProgressDialog RECV %s", ins); + uiHandler.post(new Runnable() { + @Override + public void run() { + if (type > TYPE_DEBUG) { + switch (ins) { + case INS_GET_SUBADDRESS_SPEND_PUBLIC_KEY: // lookahead + case INS_VERIFY_KEY: + case INS_GET_CHACHA8_PREKEY: + break; + default: + if (type != TYPE_SEND) + setMessage(getContext().getString(R.string.progress_ledger_progress)); + } + } else { + setMessage("Returned from " + ins.name()); + } + } + }); + } + + // TODO: we use ints in Java but the are signed; accounts & subaddresses are unsigned ... + private int bytesToInteger(byte[] bytes, int offset) { + int result = 0; + for (int i = 3; i >= 0; i--) { + result <<= 8; + result |= (bytes[offset + i] & 0xFF); + } + return result; + } +} \ No newline at end of file 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 cdc0f8f..ad0feed 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java +++ b/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java @@ -389,4 +389,5 @@ public class Wallet { return getSubaddress(accountIndex, getNumSubaddresses(accountIndex) - 1); } + public native boolean isKeyOnDevice(); } 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 2ee5a7f..41bda7f 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/model/WalletManager.java +++ b/app/src/main/java/com/m2049r/xmrwallet/model/WalletManager.java @@ -17,6 +17,7 @@ package com.m2049r.xmrwallet.model; import com.m2049r.xmrwallet.data.WalletNode; +import com.m2049r.xmrwallet.ledger.Ledger; import java.io.BufferedReader; import java.io.File; @@ -129,6 +130,23 @@ public class WalletManager { String viewKeyString, String spendKeyString); + public Wallet createWalletFromDevice(File aFile, String password, long restoreHeight, + String deviceName) { + long walletHandle = createWalletFromDeviceJ(aFile.getAbsolutePath(), password, + getNetworkType().getValue(), deviceName, restoreHeight, + Ledger.SUBADDRESS_LOOKAHEAD); + Wallet wallet = new Wallet(walletHandle); + manageWallet(wallet); + return wallet; + } + + private native long createWalletFromDeviceJ(String path, String password, + int networkType, + String deviceName, + long restoreHeight, + String subaddressLookahead); + + public native boolean closeJ(Wallet wallet); public boolean close(Wallet wallet) { @@ -150,6 +168,12 @@ public class WalletManager { public native boolean verifyWalletPassword(String keys_file_name, String password, boolean watch_only); + public boolean verifyWalletPasswordOnly(String keys_file_name, String password) { + return queryWalletHardware(keys_file_name, password) >= 0; + } + + public native int queryWalletHardware(String keys_file_name, String password); + //public native List findWallets(String path); // this does not work - some error in boost public class WalletInfo implements Comparable { diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/WalletService.java b/app/src/main/java/com/m2049r/xmrwallet/service/WalletService.java index adc08c6..e06f9e6 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/service/WalletService.java +++ b/app/src/main/java/com/m2049r/xmrwallet/service/WalletService.java @@ -221,6 +221,8 @@ public class WalletService extends Service { void onSetNotes(boolean success); void onWalletStarted(boolean success); + + void onWalletOpen(int hardware); } String progressText = null; @@ -535,6 +537,8 @@ public class WalletService extends Service { showProgress(30); if (walletMgr.walletExists(path)) { Timber.d("open wallet %s", path); + int hw = WalletManager.getInstance().queryWalletHardware(path + ".keys", walletPassword); + if (observer != null) observer.onWalletOpen(hw); wallet = walletMgr.openWallet(path, walletPassword); showProgress(60); Timber.d("wallet opened"); 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 cbfb526..48b2570 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java +++ b/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java @@ -349,33 +349,33 @@ public class Helper { 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().verifyWalletPassword(walletPath, password, true)) { + 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().verifyWalletPassword(walletPath, possibleCrazyPass, true)) { + if (WalletManager.getInstance().verifyWalletPasswordOnly(walletPath, possibleCrazyPass)) { return possibleCrazyPass; } } // generate & try with CrAzYpass String crazyPass = KeyStoreHelper.getCrazyPass(context, password); - if (WalletManager.getInstance().verifyWalletPassword(walletPath, crazyPass, true)) { + 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().verifyWalletPassword(walletPath, brokenCrazyPass2, true)) { + && WalletManager.getInstance().verifyWalletPasswordOnly(walletPath, brokenCrazyPass2)) { return brokenCrazyPass2; } String brokenCrazyPass1 = KeyStoreHelper.getBrokenCrazyPass(context, password, 1); if ((brokenCrazyPass1 != null) - && WalletManager.getInstance().verifyWalletPassword(walletPath, brokenCrazyPass1, true)) { + && WalletManager.getInstance().verifyWalletPasswordOnly(walletPath, brokenCrazyPass1)) { return brokenCrazyPass1; } @@ -407,6 +407,7 @@ public class Helper { final CancellationSignal cancelSignal = new CancellationSignal(); final AtomicBoolean incorrectSavedPass = new AtomicBoolean(false); + class LoginWalletTask extends AsyncTask { private String pass; private boolean fingerprintUsed; @@ -594,6 +595,5 @@ public class Helper { static public ExchangeApi getExchangeApi() { return new com.m2049r.xmrwallet.service.exchange.coinmarketcap.ExchangeApiImpl(OkHttpClientSingleton.getOkHttpClient()); - } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/Notice.java b/app/src/main/java/com/m2049r/xmrwallet/util/Notice.java index 1b0b1c8..1fa09e6 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/util/Notice.java +++ b/app/src/main/java/com/m2049r/xmrwallet/util/Notice.java @@ -33,6 +33,8 @@ import com.m2049r.xmrwallet.dialog.HelpFragment; import java.util.ArrayList; import java.util.List; +import timber.log.Timber; + public class Notice { private static final String PREFS_NAME = "notice"; private static List notices = null; @@ -40,6 +42,7 @@ public class Notice { private static final String NOTICE_SHOW_XMRTO_ENABLED_LOGIN = "notice_xmrto_enabled_login"; private static final String NOTICE_SHOW_XMRTO_ENABLED_SEND = "notice_xmrto_enabled_send"; private static final String NOTICE_SHOW_CRAZYPASS = "notice_crazypass_enabled_login"; + private static final String NOTICE_SHOW_LEDGER = "notice_ledger_enabled_login"; private static void init() { synchronized (Notice.class) { @@ -63,6 +66,12 @@ public class Notice { R.string.help_details, 2) ); + notices.add( + new Notice(NOTICE_SHOW_LEDGER, + R.string.info_ledger_enabled, + R.string.help_create_ledger, + 1) + ); } } diff --git a/app/src/main/res/anim/fab_close.xml b/app/src/main/res/anim/fab_close.xml index 9dd7e78..7a5c735 100644 --- a/app/src/main/res/anim/fab_close.xml +++ b/app/src/main/res/anim/fab_close.xml @@ -1,6 +1,6 @@ + android:fillAfter="false"> + android:fillAfter="false"> + + + + diff --git a/app/src/main/res/layout/dialog_ledger_progress.xml b/app/src/main/res/layout/dialog_ledger_progress.xml new file mode 100644 index 0000000..f6bff98 --- /dev/null +++ b/app/src/main/res/layout/dialog_ledger_progress.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + \ 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 569a665..15925c0 100644 --- a/app/src/main/res/layout/fragment_review.xml +++ b/app/src/main/res/layout/fragment_review.xml @@ -51,10 +51,13 @@ android:textAlignment="center" tools:text="49RBjxQ2zgf7t17w7So9ngcEY9obKzsrr6Dsah24MNSMiMBEeiYPP5CCTBq4GpZcEYN5Zf3upsLiwd5PezePE1i4Tf3rryY" /> - + android:layout_marginTop="@dimen/header_top" + android:orientation="vertical" + android:visibility="gone"> - - + + - + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/header_top" + android:orientation="vertical" + android:visibility="gone"> + + + + + + + + + + + + + android:text="@string/generate_spendkey_label" /> - - - - - - - - - + android:layout_marginTop="@dimen/data_top" + android:textAlignment="center" + tools:text="2bb6afd03f1a4b91e619236b9d342aeb6d232fa50e5b9b91a00db26d1cafaa08" /> + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_fabmenu.xml b/app/src/main/res/layout/layout_fabmenu.xml index 826c4d9..534fe3d 100644 --- a/app/src/main/res/layout/layout_fabmenu.xml +++ b/app/src/main/res/layout/layout_fabmenu.xml @@ -139,6 +139,39 @@ + + + + + + + + +

+ + + + \ No newline at end of file diff --git a/app/src/main/res/values-de/help.xml b/app/src/main/res/values-de/help.xml index bf6ad07..1580bec 100644 --- a/app/src/main/res/values-de/help.xml +++ b/app/src/main/res/values-de/help.xml @@ -204,7 +204,7 @@ BTC senden

XMR.TO

-

XXMR.TO ist ein Drittanbieter-Service, der als Wechselservice von Monero zu Bitcoin fungiert. +

XMR.TO ist ein Drittanbieter-Service, der als Wechselservice von Monero zu Bitcoin fungiert. Wir verwenden die XMR.TO Schnittstelle, um Bitcoin-Zahlungen in Monerujo zu integrieren. Bitte sieh dir https://xmr.to an und entscheide selbst, ob es etwas ist, was du verwenden möchtest. Das Monerujo Team ist nicht mit XMR.TO verbunden und kann dir bei deren Service nicht helfen.

@@ -227,4 +227,15 @@ indem du zum vorherigen Schritt zurückgehst und dann zum Bildschirm "Bestätigen" zurückkehrst.

]]>
+ Create Wallet - Ledger +

You want to recover your wallet from your Ledger Nano S device.

+

Your secret keys never leave the Ledger device, so you need it plugged in every + time you want to access your wallet.

+

Enter a unique wallet name and password. The password is used for securing your wallet data on the Android + device. Use a strong password - even better use a passphrase.

+

Enter the block number of the first transaction used for this address in the + field \"Restore Height\". You can also use a date in the format YYYY-MM-DD. If you are not sure, + enter an approximate date/blockheight before you first used this wallet address.

+ ]]>
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 5d120ff..3986943 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -34,6 +34,7 @@ BTC Zahlung aktiviert - Tippe für mehr Infos. CrAzYpass aktiviert - Tippe für mehr Infos. + Ledger aktiviert - Tippe für mehr Infos. Du hast eine BTC Adresse eingegeben.
@@ -321,4 +322,20 @@ Sprache Benutze Systemsprache + + Restore from Ledger Nano S + + Communicating with Ledger + Confirmation on Ledger required! + Retrieving subaddresses + Verifying keys + Doing crazy maths + Hashing stuff + Please (re)connect Ledger device + + Creating account + Updating wallet + + %1$s attached + %1$s detached diff --git a/app/src/main/res/values-el/help.xml b/app/src/main/res/values-el/help.xml index 9f45ffc..1a8312c 100644 --- a/app/src/main/res/values-el/help.xml +++ b/app/src/main/res/values-el/help.xml @@ -213,4 +213,15 @@ πίσω στο προηγούμενο βήμα και μετά επιστρέφοντας στην οθόνη \"Επιβεβαίωση\".

]]>
+ Create Wallet - Ledger +

You want to recover your wallet from your Ledger Nano S device.

+

Your secret keys never leave the Ledger device, so you need it plugged in every + time you want to access your wallet.

+

Enter a unique wallet name and password. The password is used for securing your wallet data on the Android + device. Use a strong password - even better use a passphrase.

+

Enter the block number of the first transaction used for this address in the + field \"Restore Height\". You can also use a date in the format YYYY-MM-DD. If you are not sure, + enter an approximate date/blockheight before you first used this wallet address.

+ ]]>
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 831a1ee..8fdd619 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -32,6 +32,8 @@ Υψηλότερη προτεραιότητα = Υψηλότερα Κόμιστρα Συναλλαγή BTC ενεργοποιήθηκε, πάτα για περισσότερες πληροφορείες. + CrAzYpass ενεργοποιήθηκε, πάτα για περισσότερες πληροφορείες. + Ledger ενεργοποιήθηκε, πάτα για περισσότερες πληροφορείες. Έβαλες μια διεύθυνση bitcoin.
@@ -284,7 +286,6 @@ Επαναφορά πορτοφολιού από σπόρο 25-λέξεων Change Passphrase - CrAzYpass enabled, tap for more info. Change Password in progress Change Password failed! Password changed @@ -320,4 +321,20 @@ Language Use System Language + + Restore from Ledger Nano S + + Communicating with Ledger + Confirmation on Ledger required! + Retrieving subaddresses + Verifying keys + Doing crazy maths + Hashing stuff + Please (re)connect Ledger device + + Creating account + Updating wallet + + %1$s attached + %1$s detached diff --git a/app/src/main/res/values-es/help.xml b/app/src/main/res/values-es/help.xml index d0d1c51..fe483b7 100644 --- a/app/src/main/res/values-es/help.xml +++ b/app/src/main/res/values-es/help.xml @@ -251,4 +251,16 @@ parte de XMR.TO, esto se logra dando un paso atrás y luego volviendo a la pantalla de \"Confirmar\".

]]>
+ + Create Wallet - Ledger +

You want to recover your wallet from your Ledger Nano S device.

+

Your secret keys never leave the Ledger device, so you need it plugged in every + time you want to access your wallet.

+

Enter a unique wallet name and password. The password is used for securing your wallet data on the Android + device. Use a strong password - even better use a passphrase.

+

Enter the block number of the first transaction used for this address in the + field \"Restore Height\". You can also use a date in the format YYYY-MM-DD. If you are not sure, + enter an approximate date/blockheight before you first used this wallet address.

+ ]]>
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index d331f2c..8195dde 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -292,8 +292,10 @@ Monto\n(BTC) Soy monerujo Orden XMR.TO + Pago en BTC activado, toca para más info. CrAzYpass activado, toca para más info. + Ledger activado, toca para más info. Crear Cuenta Cuentas @@ -306,4 +308,20 @@ Language Use System Language + + Restore from Ledger Nano S + + Communicating with Ledger + Confirmation on Ledger required! + Retrieving subaddresses + Verifying keys + Doing crazy maths + Hashing stuff + Please (re)connect Ledger device + + Creating account + Updating wallet + + %1$s attached + %1$s detached diff --git a/app/src/main/res/values-fr/help.xml b/app/src/main/res/values-fr/help.xml index 386e511..bdb1f3c 100644 --- a/app/src/main/res/values-fr/help.xml +++ b/app/src/main/res/values-fr/help.xml @@ -244,4 +244,15 @@ XMR.TO en retournant à l’étape précédente puis en revenant à l’écran \"Confirmation\".

]]> + Create Wallet - Ledger +

You want to recover your wallet from your Ledger Nano S device.

+

Your secret keys never leave the Ledger device, so you need it plugged in every + time you want to access your wallet.

+

Enter a unique wallet name and password. The password is used for securing your wallet data on the Android + device. Use a strong password - even better use a passphrase.

+

Enter the block number of the first transaction used for this address in the + field \"Restore Height\". You can also use a date in the format YYYY-MM-DD. If you are not sure, + enter an approximate date/blockheight before you first used this wallet address.

+ ]]>
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index c6ab669..5635e4c 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -33,8 +33,8 @@ Plus Prioritaire = Plus de Frais Paiement BTC activé, tapez pour plus d\'infos. - CrAzYpass activé, tapez pour plus d\'infos. + Ledger activé, tapez pour plus d\'infos. Vous avez entré une adresse Bitcoin.
@@ -324,4 +324,20 @@ Language Use System Language + + Restore from Ledger Nano S + + Communicating with Ledger + Confirmation on Ledger required! + Retrieving subaddresses + Verifying keys + Doing crazy maths + Hashing stuff + Please (re)connect Ledger device + + Creating account + Updating wallet + + %1$s attached + %1$s detached diff --git a/app/src/main/res/values-hu/help.xml b/app/src/main/res/values-hu/help.xml index fe1fece..cf4b767 100644 --- a/app/src/main/res/values-hu/help.xml +++ b/app/src/main/res/values-hu/help.xml @@ -234,4 +234,15 @@ visszamész az előző lépésre, majd visszajössz a Megerősítés oldalra.

]]>
+ Create Wallet - Ledger +

You want to recover your wallet from your Ledger Nano S device.

+

Your secret keys never leave the Ledger device, so you need it plugged in every + time you want to access your wallet.

+

Enter a unique wallet name and password. The password is used for securing your wallet data on the Android + device. Use a strong password - even better use a passphrase.

+

Enter the block number of the first transaction used for this address in the + field \"Restore Height\". You can also use a date in the format YYYY-MM-DD. If you are not sure, + enter an approximate date/blockheight before you first used this wallet address.

+ ]]>
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index adf6303..3a38ace 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -34,6 +34,7 @@ BTC fizetés engedélyezve, koppints ide a részletekért. CrAzYpass engedélyezve, koppints ide a részletekért + Ledger engedélyezve, koppints ide a részletekért Bitcoin-címet adtál meg.
@@ -44,7 +45,6 @@ %1$s BTC - Megerősítés folyamatban Kifizetés folyamatban XMR.TO-hiba (%1$s) @@ -322,4 +322,20 @@ Nyelv Rendszernyelv használata + + Restore from Ledger Nano S + + Communicating with Ledger + Confirmation on Ledger required! + Retrieving subaddresses + Verifying keys + Doing crazy maths + Hashing stuff + Please (re)connect Ledger device + + Creating account + Updating wallet + + %1$s attached + %1$s detached diff --git a/app/src/main/res/values-it/help.xml b/app/src/main/res/values-it/help.xml index f446dfa..42af988 100644 --- a/app/src/main/res/values-it/help.xml +++ b/app/src/main/res/values-it/help.xml @@ -177,4 +177,15 @@

Non appena il conto alla rovescia arriva a zero, è necessario richiedere una nuova quotazione a XMR.TO tornando indietro al passo precedente e tornando poi di nuovo alla schermata \"Conferma\".

]]>
+ Create Wallet - Ledger +

You want to recover your wallet from your Ledger Nano S device.

+

Your secret keys never leave the Ledger device, so you need it plugged in every + time you want to access your wallet.

+

Enter a unique wallet name and password. The password is used for securing your wallet data on the Android + device. Use a strong password - even better use a passphrase.

+

Enter the block number of the first transaction used for this address in the + field \"Restore Height\". You can also use a date in the format YYYY-MM-DD. If you are not sure, + enter an approximate date/blockheight before you first used this wallet address.

+ ]]>
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 695dabb..b7b03f8 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -34,6 +34,7 @@ Pagamento BTC abilitato, tocca per maggiori informazioni. CrAzYpass abilitato, tocca per maggiori informazioni. + Ledger abilitato, tocca per maggiori informazioni. Hai inserito un indirizzo Bitcoin.
@@ -322,4 +323,20 @@ Language Use System Language + + Restore from Ledger Nano S + + Communicating with Ledger + Confirmation on Ledger required! + Retrieving subaddresses + Verifying keys + Doing crazy maths + Hashing stuff + Please (re)connect Ledger device + + Creating account + Updating wallet + + %1$s attached + %1$s detached diff --git a/app/src/main/res/values-nb/help.xml b/app/src/main/res/values-nb/help.xml index 226efa9..a1410d8 100644 --- a/app/src/main/res/values-nb/help.xml +++ b/app/src/main/res/values-nb/help.xml @@ -233,4 +233,15 @@ det tidligere steget og komme tilbake til \"Bekreft\" skjermen.

]]>
+ Create Wallet - Ledger +

You want to recover your wallet from your Ledger Nano S device.

+

Your secret keys never leave the Ledger device, so you need it plugged in every + time you want to access your wallet.

+

Enter a unique wallet name and password. The password is used for securing your wallet data on the Android + device. Use a strong password - even better use a passphrase.

+

Enter the block number of the first transaction used for this address in the + field \"Restore Height\". You can also use a date in the format YYYY-MM-DD. If you are not sure, + enter an approximate date/blockheight before you first used this wallet address.

+ ]]>
diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 6eb5ccc..2ba5890 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -34,6 +34,7 @@ BTC betaling tilgjengelig, trykk for mer info. CrAzYpass tilgjengelig, trykk for mer info. + Ledger tilgjengelig, trykk for mer info. Du skrev inn en Bitcoin addresse.
@@ -320,4 +321,20 @@ Language Use System Language + + Restore from Ledger Nano S + + Communicating with Ledger + Confirmation on Ledger required! + Retrieving subaddresses + Verifying keys + Doing crazy maths + Hashing stuff + Please (re)connect Ledger device + + Creating account + Updating wallet + + %1$s attached + %1$s detached diff --git a/app/src/main/res/values-pt/help.xml b/app/src/main/res/values-pt/help.xml index 5e0533b..8583f5e 100644 --- a/app/src/main/res/values-pt/help.xml +++ b/app/src/main/res/values-pt/help.xml @@ -236,4 +236,15 @@ indo ao passo anterior e depois voltando ao ecrã de \"Confirmar\".

]]>
+ Create Wallet - Ledger +

You want to recover your wallet from your Ledger Nano S device.

+

Your secret keys never leave the Ledger device, so you need it plugged in every + time you want to access your wallet.

+

Enter a unique wallet name and password. The password is used for securing your wallet data on the Android + device. Use a strong password - even better use a passphrase.

+

Enter the block number of the first transaction used for this address in the + field \"Restore Height\". You can also use a date in the format YYYY-MM-DD. If you are not sure, + enter an approximate date/blockheight before you first used this wallet address.

+ ]]>
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index d60b332..aeed5bc 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -33,8 +33,8 @@ Prioridade Alta = Taxas Altas Pagamento em BTC activado, toca para mais informação. - passLoUCa activa, toca para mais informação. + Ledger activa, toca para mais informação. Introduziu um endereço Bitcoin.
@@ -324,4 +324,20 @@ Language Use System Language + + Restore from Ledger Nano S + + Communicating with Ledger + Confirmation on Ledger required! + Retrieving subaddresses + Verifying keys + Doing crazy maths + Hashing stuff + Please (re)connect Ledger device + + Creating account + Updating wallet + + %1$s attached + %1$s detached diff --git a/app/src/main/res/values-ro/help.xml b/app/src/main/res/values-ro/help.xml index b6c503f..d7b3dbb 100644 --- a/app/src/main/res/values-ro/help.xml +++ b/app/src/main/res/values-ro/help.xml @@ -221,4 +221,15 @@ înapoi la pasul anterior apoi revenind la ecranul \"Confirm\".

]]>
+ Create Wallet - Ledger +

You want to recover your wallet from your Ledger Nano S device.

+

Your secret keys never leave the Ledger device, so you need it plugged in every + time you want to access your wallet.

+

Enter a unique wallet name and password. The password is used for securing your wallet data on the Android + device. Use a strong password - even better use a passphrase.

+

Enter the block number of the first transaction used for this address in the + field \"Restore Height\". You can also use a date in the format YYYY-MM-DD. If you are not sure, + enter an approximate date/blockheight before you first used this wallet address.

+ ]]>
diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 351e1f5..2d19018 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -32,6 +32,8 @@ Prioritate mare = Comision mare Plată BTC activată, apasă pentru mai multe informații. + CrAzYpass activată, apasă pentru mai multe informații. + Ledger activată, apasă pentru mai multe informații. Ai introdus o adresă de Bitcoin.
@@ -284,7 +286,6 @@ Restaurează portofel folosind cele 25 de cuvinte mnemonice Change Passphrase - CrAzYpass enabled, tap for more info. Change Password in progress Change Password failed! Password changed @@ -320,4 +321,20 @@ Language Use System Language + + Restore from Ledger Nano S + + Communicating with Ledger + Confirmation on Ledger required! + Retrieving subaddresses + Verifying keys + Doing crazy maths + Hashing stuff + Please (re)connect Ledger device + + Creating account + Updating wallet + + %1$s attached + %1$s detached diff --git a/app/src/main/res/values-ru/help.xml b/app/src/main/res/values-ru/help.xml index 64a3d96..6c01f12 100644 --- a/app/src/main/res/values-ru/help.xml +++ b/app/src/main/res/values-ru/help.xml @@ -239,4 +239,15 @@ предложение, вернувшись к предыдущему шагу, а затем к экрану \"Подтверждение\".

]]>
+ Create Wallet - Ledger +

You want to recover your wallet from your Ledger Nano S device.

+

Your secret keys never leave the Ledger device, so you need it plugged in every + time you want to access your wallet.

+

Enter a unique wallet name and password. The password is used for securing your wallet data on the Android + device. Use a strong password - even better use a passphrase.

+

Enter the block number of the first transaction used for this address in the + field \"Restore Height\". You can also use a date in the format YYYY-MM-DD. If you are not sure, + enter an approximate date/blockheight before you first used this wallet address.

+ ]]>
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index cdb37f5..a45a913 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -33,8 +33,8 @@ Высокий приоритет = Высокие комиссии Доступны переводы в BTC, нажмите для доп. информации - Доступен CrAzYpass, нажмите для доп. информации + Доступен Ledger, нажмите для доп. информации Вы ввели Bitcoin адрес.
@@ -323,4 +323,20 @@ Language Use System Language + + Restore from Ledger Nano S + + Communicating with Ledger + Confirmation on Ledger required! + Retrieving subaddresses + Verifying keys + Doing crazy maths + Hashing stuff + Please (re)connect Ledger device + + Creating account + Updating wallet + + %1$s attached + %1$s detached diff --git a/app/src/main/res/values-sv/help.xml b/app/src/main/res/values-sv/help.xml index f96d32e..c1cc146 100644 --- a/app/src/main/res/values-sv/help.xml +++ b/app/src/main/res/values-sv/help.xml @@ -214,4 +214,15 @@ tillbaka till den tidigare skärmen och sedan gå till \"Bekräfta\"skärmen.

]]>
+ Create Wallet - Ledger +

You want to recover your wallet from your Ledger Nano S device.

+

Your secret keys never leave the Ledger device, so you need it plugged in every + time you want to access your wallet.

+

Enter a unique wallet name and password. The password is used for securing your wallet data on the Android + device. Use a strong password - even better use a passphrase.

+

Enter the block number of the first transaction used for this address in the + field \"Restore Height\". You can also use a date in the format YYYY-MM-DD. If you are not sure, + enter an approximate date/blockheight before you first used this wallet address.

+ ]]>
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 7fd0c1d..d0651b2 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -34,8 +34,8 @@ Högre prioritet = Högre avgift BTC-betalning aktiverad, tryck för mer info. - CrAzYpass aktiverat, tryck för mer info. + Ledger aktiverat, tryck för mer info. Du har angivit en Bitcoin-adress.
Du kommer att skicka XMR och mottagaren får BTC via tjänsten XMR.TO.]]>
@@ -305,4 +305,20 @@ Language Use System Language + + Restore from Ledger Nano S + + Communicating with Ledger + Confirmation on Ledger required! + Retrieving subaddresses + Verifying keys + Doing crazy maths + Hashing stuff + Please (re)connect Ledger device + + Creating account + Updating wallet + + %1$s attached + %1$s detached diff --git a/app/src/main/res/values-zh-rCN/help.xml b/app/src/main/res/values-zh-rCN/help.xml index efbe703..17b22e4 100644 --- a/app/src/main/res/values-zh-rCN/help.xml +++ b/app/src/main/res/values-zh-rCN/help.xml @@ -189,4 +189,15 @@

当倒数计时归零的时候,你将会需要回到上一步再回到\"确认\"页面重新向XMR.TO寻求汇率报价

]]> + Create Wallet - Ledger +

You want to recover your wallet from your Ledger Nano S device.

+

Your secret keys never leave the Ledger device, so you need it plugged in every + time you want to access your wallet.

+

Enter a unique wallet name and password. The password is used for securing your wallet data on the Android + device. Use a strong password - even better use a passphrase.

+

Enter the block number of the first transaction used for this address in the + field \"Restore Height\". You can also use a date in the format YYYY-MM-DD. If you are not sure, + enter an approximate date/blockheight before you first used this wallet address.

+ ]]>
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 3a49451..a0d3bec 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -34,6 +34,7 @@ BTC付款已启用, 点选了解更多 CrAzYpass已启用, 点选了解更多 + Ledger已启用, 点选了解更多 你输入了Bitcoin地址
@@ -318,4 +319,20 @@ 语言 使用系统语言 + + Restore from Ledger Nano S + + Communicating with Ledger + Confirmation on Ledger required! + Retrieving subaddresses + Verifying keys + Doing crazy maths + Hashing stuff + Please (re)connect Ledger device + + Creating account + Updating wallet + + %1$s attached + %1$s detached diff --git a/app/src/main/res/values-zh-rTW/help.xml b/app/src/main/res/values-zh-rTW/help.xml index 17ffad6..3d13f50 100644 --- a/app/src/main/res/values-zh-rTW/help.xml +++ b/app/src/main/res/values-zh-rTW/help.xml @@ -189,4 +189,15 @@

當倒數計時歸零的時候,你將會需要回到上一步再回到\"確認\"頁面重新向XMR.TO尋求匯率報價

]]>
+ Create Wallet - Ledger +

You want to recover your wallet from your Ledger Nano S device.

+

Your secret keys never leave the Ledger device, so you need it plugged in every + time you want to access your wallet.

+

Enter a unique wallet name and password. The password is used for securing your wallet data on the Android + device. Use a strong password - even better use a passphrase.

+

Enter the block number of the first transaction used for this address in the + field \"Restore Height\". You can also use a date in the format YYYY-MM-DD. If you are not sure, + enter an approximate date/blockheight before you first used this wallet address.

+ ]]>
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 5a331c4..b71d37f 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -34,6 +34,7 @@ BTC付款已啟用, 點選了解更多 CrAzYpass已啟用, 點選了解更多 + Ledger已啟用, 點選了解更多 你輸入了Bitcoin地址
@@ -319,4 +320,20 @@ 語言 使用系統語言 + + Restore from Ledger Nano S + + Communicating with Ledger + Confirmation on Ledger required! + Retrieving subaddresses + Verifying keys + Doing crazy maths + Hashing stuff + Please (re)connect Ledger device + + Creating account + Updating wallet + + %1$s attached + %1$s detached diff --git a/app/src/main/res/values/help.xml b/app/src/main/res/values/help.xml index 0ab7395..f74b610 100644 --- a/app/src/main/res/values/help.xml +++ b/app/src/main/res/values/help.xml @@ -26,9 +26,21 @@

Enter a unique wallet name and password. The password is used for securing your wallet data on the device. Use a strong password - even better use a passphrase.

Enter your Seed in the field \"Mnemonic Seed\".

-

If you know the block number of the first transaction used for this address, enter it in the - field \"Restore Height\" - leaving it blank will scan the entire blockchain for - transactions belonging to your address. This takes a long time.

+

Enter the block number of the first transaction used for this address in the + field \"Restore Height\". You can also use a date in the format YYYY-MM-DD. If you are not sure, + enter an approximate date/blockheight before you first used this wallet address.

+ ]]>
+ + Create Wallet - Ledger +

You want to recover your wallet from your Ledger Nano S device.

+

Your secret keys never leave the Ledger device, so you need it plugged in every + time you want to access your wallet.

+

Enter a unique wallet name and password. The password is used for securing your wallet data on the Android + device. Use a strong password - even better use a passphrase.

+

Enter the block number of the first transaction used for this address in the + field \"Restore Height\". You can also use a date in the format YYYY-MM-DD. If you are not sure, + enter an approximate date/blockheight before you first used this wallet address.

]]>
Enter a unique wallet name and password. The password is used for securing your wallet data on the device. Use a strong password - even better use a passphrase.

Enter your Monero Address in the field \"Public Address\" and fill out \"View Key\" and \"Spend Key\".

-

If you know the block number of the first transaction used for this address, enter it in the - field \"Restore Height\" - leaving it blank will scan the entire blockchain for - transactions belonging to your address. This takes a long time.

+

Enter the block number of the first transaction used for this address in the + field \"Restore Height\". You can also use a date in the format YYYY-MM-DD. If you are not sure, + enter an approximate date/blockheight before you first used this wallet address.

]]>
Enter a unique wallet name and password. The password is used for securing your wallet data on the device. Use a strong password - even better use a passphrase.

Enter your Monero Address in the field \"Public Address\" and fill out the \"View Key\".

-

If you know the block number of the first transaction used for this address, enter it in the - field \"Restore Height\" - leaving it blank will scan the entire blockchain for - transactions belonging to your address. This takes a long time.

+

Enter the block number of the first transaction used for this address in the + field \"Restore Height\". You can also use a date in the format YYYY-MM-DD. If you are not sure, + enter an approximate date/blockheight before you first used this wallet address.

]]>
+ extremely difficult to hack!
This feature is mandatory for all newly created wallets.

Legacy Password

If you see your passphrase here, your wallet files are not as secure as when using @@ -232,5 +244,4 @@

Once the countdown reaches zero, you need to get a new quote from XMR.TO by going back to the previous step and then coming back to the \"Confirm\" screen.

]]>
- diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d50423c..364ebb4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -35,8 +35,8 @@ Higher Priority = Higher Fees BTC payment enabled, tap for more info. - CrAzYpass enabled, tap for more info. + Ledger enabled, tap for more info. You entered a Bitcoin address.
@@ -47,7 +47,6 @@ %1$s BTC - Confirmation Pending Payment Pending XMR.TO Error (%1$s) @@ -209,6 +208,7 @@ New Seed View + Ledger Public Address View Key @@ -368,4 +368,20 @@ Language Use System Language + + Restore from Ledger Nano S + + Communicating with Ledger + Confirmation on Ledger required! + Retrieving subaddresses + Verifying keys + Doing crazy maths + Hashing stuff + Please (re)connect Ledger device + + Creating account + Updating wallet + + %1$s attached + %1$s detached diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 989031d..9aa7aeb 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -329,5 +329,4 @@ - diff --git a/app/src/main/res/xml/usb_device_filter.xml b/app/src/main/res/xml/usb_device_filter.xml new file mode 100644 index 0000000..2fc6025 --- /dev/null +++ b/app/src/main/res/xml/usb_device_filter.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/external-libs/monero/include/wallet2_api.h b/external-libs/monero/include/wallet2_api.h index 546ce16..e47a890 100644 --- a/external-libs/monero/include/wallet2_api.h +++ b/external-libs/monero/include/wallet2_api.h @@ -802,6 +802,12 @@ struct Wallet //! Initiates a light wallet import wallet request virtual bool lightWalletImportWalletRequest(std::string &payment_id, uint64_t &fee, bool &new_request, bool &request_fulfilled, std::string &payment_address, std::string &status) = 0; + + /*! + * \brief Queries if the wallet keys are on a hardware device + * \return true if they are + */ + virtual bool isKeyOnDevice() const = 0; }; /** @@ -975,6 +981,17 @@ struct WalletManager */ virtual bool verifyWalletPassword(const std::string &keys_file_name, const std::string &password, bool no_spend_key) const = 0; + /*! + * \brief determine the key storage for the specified wallet file + * \param keys_file_name Keys file to verify password for + * \param password Password to verify + * \return -1: incorrect password, 0 = default hw, 1 ledger hw + * + * for verification only - determines key storage hardware + * + */ + virtual int queryWalletHardware(const std::string &keys_file_name, const std::string &password) const = 0; + /*! * \brief findWallets - searches for the wallet files by given path name recursively * \param path - starting point to search