Migrate from password manager to key chain (#36)

* Update gradle
Change .gitignore & clean project

* Change password manager

* Change password manager
pull/2/head
Maksim 7 years ago committed by Marat Subkhankulov
parent 71f0f79fb9
commit 3402342531
  1. 3
      .gitignore
  2. 22
      .idea/compiler.xml
  3. 3
      .idea/copyright/profiles_settings.xml
  4. 19
      .idea/gradle.xml
  5. 36
      .idea/misc.xml
  6. 10
      .idea/modules.xml
  7. 12
      .idea/runConfigurations.xml
  8. 7
      .idea/vcs.xml
  9. 2
      app/build.gradle
  10. 151
      app/src/main/java/com/wallet/crypto/trustapp/controller/Controller.java
  11. 30
      app/src/main/java/com/wallet/crypto/trustapp/controller/ServiceErrorException.java
  12. 308
      app/src/main/java/com/wallet/crypto/trustapp/util/KS.java
  13. 31
      app/src/main/java/com/wallet/crypto/trustapp/util/PMMigrateHelper.java
  14. 34
      app/src/main/java/com/wallet/crypto/trustapp/views/CreateAccountActivity.java
  15. 21
      app/src/main/java/com/wallet/crypto/trustapp/views/ExportAccountActivity.java
  16. 7
      app/src/main/java/com/wallet/crypto/trustapp/views/ImportKeystoreFragment.java
  17. 246
      app/src/main/java/com/wallet/crypto/trustapp/views/SendActivity.java
  18. 2
      app/src/main/java/com/wallet/crypto/trustapp/views/SplashActivity.java
  19. 54
      app/src/main/java/com/wallet/crypto/trustapp/views/TransactionListActivity.java
  20. 27
      app/src/main/res/layout/layout_send_progress.xml
  21. 7
      app/src/main/res/values/strings.xml
  22. 2
      build.gradle

3
.gitignore vendored

@ -1,5 +1,4 @@
/.idea/workspace.xml
/.idea/libraries
.idea
.DS_Store
/captures
.externalNativeBuild

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<resourceExtensions />
<wildcardResourcePatterns>
<entry name="!?*.java" />
<entry name="!?*.form" />
<entry name="!?*.class" />
<entry name="!?*.groovy" />
<entry name="!?*.scala" />
<entry name="!?*.flex" />
<entry name="!?*.kt" />
<entry name="!?*.clj" />
<entry name="!?*.aj" />
</wildcardResourcePatterns>
<annotationProcessing>
<profile default="true" name="Default" enabled="false">
<processorPath useClasspath="true" />
</profile>
</annotationProcessing>
</component>
</project>

@ -1,3 +0,0 @@
<component name="CopyrightManager">
<settings default="" />
</component>

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/tn" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
<component name="SvnBranchConfigurationManager">
<option name="mySupportsUserInfoFilter" value="true" />
</component>
</project>

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
<module fileurl="file://$PROJECT_DIR$/tn/tn.iml" filepath="$PROJECT_DIR$/tn/tn.iml" />
<module fileurl="file://$PROJECT_DIR$/trust-wallet-android.iml" filepath="$PROJECT_DIR$/trust-wallet-android.iml" />
</modules>
</component>
</project>

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/app" vcs="svn" />
</component>
</project>

@ -5,7 +5,7 @@ android {
buildToolsVersion '26.0.2'
defaultConfig {
applicationId "com.wallet.crypto.trustapp"
minSdkVersion 15
minSdkVersion 23
targetSdkVersion 26
versionCode 15
versionName "1.3.11"

@ -12,6 +12,7 @@ import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.util.Log;
import android.widget.Toast;
@ -25,6 +26,7 @@ import com.wallet.crypto.trustapp.model.ESTransaction;
import com.wallet.crypto.trustapp.model.ESTransactionListResponse;
import com.wallet.crypto.trustapp.model.VMAccount;
import com.wallet.crypto.trustapp.model.VMNetwork;
import com.wallet.crypto.trustapp.util.KS;
import com.wallet.crypto.trustapp.views.AccountListActivity;
import com.wallet.crypto.trustapp.views.CreateAccountActivity;
import com.wallet.crypto.trustapp.views.ExportAccountActivity;
@ -35,7 +37,6 @@ import com.wallet.crypto.trustapp.views.TokenListActivity;
import com.wallet.crypto.trustapp.views.TransactionListActivity;
import com.wallet.crypto.trustapp.views.SendActivity;
import com.wallet.crypto.trustapp.views.WarningBackupActivity;
import com.wallet.pwd.trustapp.PasswordManager;
import org.ethereum.geth.Account;
import org.web3j.abi.FunctionEncoder;
@ -74,9 +75,6 @@ import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
import static android.os.Process.THREAD_PRIORITY_MORE_FAVORABLE;
/**
* Created by marat on 9/26/17.
* Controller class which contains all business logic
@ -89,6 +87,7 @@ public class Controller {
private static Controller mInstance;
public static final int IMPORT_ACCOUNT_REQUEST = 1;
public static final int UNLOCK_SCREEN_REQUEST = 1001;
public static final int SHARE_RESULT = 2;
private static String TAG = "CONTROLLER";
@ -362,46 +361,62 @@ public class Controller {
new ImportPrivateKeyTask(activity, privateKey, password, listener).execute();
}
public void clickSend(String from, String to, String ethAmount, OnTaskCompleted listener) {
public void clickSend(String from, String to, String ethAmount, OnTaskCompleted listener) throws ServiceErrorException {
Log.d(TAG, String.format("Send ETH: %s, %s, %s", from, to, ethAmount));
try {
String wei = EthToWei(ethAmount);
String password = PasswordManager.getPassword(from, mAppContext);
new SendTransactionTask(from, to, wei, password, null, listener).execute();
String wei = EthToWei(ethAmount);
// String password = PasswordManager.getPassword(from, mAppContext);
String password = new String(KS.get(mAppContext, from.toLowerCase()));
new SendTransactionTask(from, to, wei, password, null, listener).execute();
} catch (ServiceErrorException ex) {
Log.e(TAG, "Error sending transaction: ", ex);
throw ex;
} catch (Exception e) {
Log.e(TAG, "Error sending transaction: ", e);
}
}
public void clickSendTokens(String from, String to, String contractAddress, String tokenAmount, int decimals, OnTaskCompleted listener) {
public void clickSendTokens(String from, String to, String contractAddress, String tokenAmount, int decimals, OnTaskCompleted listener) throws ServiceErrorException {
Log.d(TAG, String.format("Send tokens: %s, %s, %s", from, to, tokenAmount));
try {
BigInteger nTokens = new BigDecimal(tokenAmount).multiply(BigDecimal.valueOf((long)Math.pow(10, decimals))).toBigInteger();
BigInteger nTokens = new BigDecimal(tokenAmount).multiply(BigDecimal.valueOf((long) Math.pow(10, decimals))).toBigInteger();
List<Type> params = Arrays.<Type>asList(new Address(to), new Uint256(nTokens));
List<Type> params = Arrays.<Type>asList(new Address(to), new Uint256(nTokens));
List<TypeReference<?>> returnTypes = Arrays.<TypeReference<?>>asList(new TypeReference<Bool>() { });
List<TypeReference<?>> returnTypes = Arrays.<TypeReference<?>>asList(new TypeReference<Bool>() {
});
Function function = new Function("transfer", params, returnTypes);
String encodedFunction = FunctionEncoder.encode(function);
byte[] data = Numeric.hexStringToByteArray(Numeric.cleanHexPrefix(encodedFunction));
String password = PasswordManager.getPassword(from, mAppContext);
new SendTransactionTask(from, contractAddress, "0", password, data, listener).execute();
Function function = new Function("transfer", params, returnTypes);
String encodedFunction = FunctionEncoder.encode(function);
byte[] data = Numeric.hexStringToByteArray(Numeric.cleanHexPrefix(encodedFunction));
// String password = PasswordManager.getPassword(from, mAppContext);
String password = new String(KS.get(mAppContext, from.toLowerCase()));
new SendTransactionTask(from, contractAddress, "0", password, data, listener).execute();
} catch (ServiceErrorException ex) {
throw ex;
} catch (Exception e) {
Log.e(TAG, "Error sending transaction: ", e);
}
}
public VMAccount createAccount(String password) {
try {
String address = new CreateAccountTask().execute(password).get();
return new VMAccount(address, "0");
} catch (Exception e) {
Log.d(TAG, e.toString());
}
return null;
public VMAccount createAccount(String password) throws Exception {
Log.d("INFO", "Trying to generate wallet in " + mKeystoreBaseDir);
String address = null;
VMAccount result = null;
try {
Account account = mEtherStore.createAccount(password);
address = account.getAddress().getHex().toLowerCase();
KS.put(mAppContext, address, password);
result = new VMAccount(address, "0");
return result;
} finally {
if (result == null && !TextUtils.isEmpty(address)) {
try {
mEtherStore.deleteAccount(address, password);
} catch (Exception e) { /* Quietly */ }
}
}
}
public List<VMAccount> getAccounts() {
@ -460,7 +475,8 @@ public class Controller {
}
public void deleteAccount(String address) throws Exception {
String password = PasswordManager.getPassword(address, mAppContext);
// String password = PasswordManager.getPassword(address, mAppContext);
String password = new String(KS.get(mAppContext, address.toLowerCase()));
mEtherStore.deleteAccount(address, password);
loadAccounts();
if (address.equals(mCurrentAddress)) {
@ -476,11 +492,14 @@ public class Controller {
parent.startActivityForResult(intent, SHARE_RESULT);
}
public String clickExportAccount(Context context, String address, String new_password) {
public String clickExportAccount(Context context, String address, String new_password) throws ServiceErrorException {
try {
Account account = mEtherStore.getAccount(address);
String account_password = PasswordManager.getPassword(address, mAppContext);
return mEtherStore.exportAccount(account, account_password, new_password);
Account account = mEtherStore.getAccount(address);
// String account_password = PasswordManager.getPassword(address, mAppContext);
String account_password = new String(KS.get(mAppContext, address.toLowerCase()));
return mEtherStore.exportAccount(account, account_password, new_password);
} catch (ServiceErrorException ex) {
throw ex;
} catch (Exception e) {
Toast.makeText(context, "Failed to export account " + e.getMessage(), Toast.LENGTH_SHORT);
}
@ -675,12 +694,25 @@ public class Controller {
}
protected Void doInBackground(String... params) {
String address = null;
try {
Account account = mEtherStore.importKeyStore(keystoreJson, password);
PasswordManager.setPassword(account.getAddress().getHex().toLowerCase(), password, mAppContext);
loadAccounts();
Log.d("INFO", "Imported account: " + account.getAddress().getHex());
listener.onTaskCompleted(new TaskResult(TaskStatus.SUCCESS, "Imported wallet."));
Account account = mEtherStore.importKeyStore(keystoreJson, password);
address = account.getAddress().getHex().toLowerCase();
KS.put(mAppContext, address, password);
loadAccounts();
Log.d("INFO", "Imported account: " + account.getAddress().getHex());
listener.onTaskCompleted(new TaskResult(TaskStatus.SUCCESS, "Imported wallet."));
} catch (ServiceErrorException e) {
if (!TextUtils.isEmpty(address)) {
try {
mEtherStore.deleteAccount(address, password);
} catch (Exception ex) {};
}
if (e.code == ServiceErrorException.USER_NOT_AUTHENTICATED) {
listener.onTaskCompleted(new TaskResult(TaskStatus.FAILURE, ""));
}
} catch (Exception e) {
Log.d("ERROR", e.toString());
listener.onTaskCompleted(new TaskResult(TaskStatus.FAILURE, "Failed to import wallet: '%s'".format(e.getMessage())));
@ -689,21 +721,38 @@ public class Controller {
}
}
private class CreateAccountTask extends AsyncTask<String, Void, String> {
protected String doInBackground(String... passwords) {
Log.d("INFO", "Trying to generate wallet in " + mKeystoreBaseDir);
String address = "";
try {
Account account = mEtherStore.createAccount(passwords[0]);
address = account.getAddress().getHex().toString().toLowerCase();
PasswordManager.setPassword(address, passwords[0], mAppContext);
} catch (Exception e) {
Log.d("ERROR", "Error generating wallet: " + e.toString());
}
return address;
}
}
// private class CreateAccountTask extends AsyncTask<String, Void, String> {
// private final OnTaskCompleted listener;
//
// public CreateAccountTask(OnTaskCompleted listener) {
// this.listener = listener;
// }
//
// protected String doInBackground(String... passwords) {
// Log.d("INFO", "Trying to generate wallet in " + mKeystoreBaseDir);
// String address = "";
// try {
// Account account = mEtherStore.createAccount(passwords[0]);
// address = account.getAddress().getHex().toString().toLowerCase();
// KS.put(mAppContext, address, passwords[0]);
// } catch (ServiceErrorException ex) {
// if (!TextUtils.isEmpty(address)) {
// try {
// mEtherStore.deleteAccount(address, passwords[0]);
// } catch (Exception e) { /* Quietly */ }
// }
//
// if (ex.code == ServiceErrorException.USER_NOT_AUTHENTICATED) {
// listener.onTaskCompleted(new TaskResult(TaskStatus.FAILURE, "USER_NOT_AUTHENTICATED"));
// }
// address = null;
// } catch (Exception e) {
// Log.d("ERROR", "Error generating wallet: " + e.toString());
// address = null;
// }
// return address;
// }
// }
// Randomize etherscan key selection stay below rate limit
private String getRandomEtherscanKey() {

@ -0,0 +1,30 @@
package com.wallet.crypto.trustapp.controller;
import android.support.annotation.Nullable;
public class ServiceErrorException extends Exception {
public static final int INVALID_DATA = 1;
public static final int KEY_STORE_ERROR = 1001;
public static final int FAIL_TO_SAVE_IV_FILE = 1002;
public static final int KEY_STORE_SECRET = 1003;
public static final int USER_NOT_AUTHENTICATED = 1004;
public static final int KEY_IS_GONE = 1005;
public static final int IV_OR_ALIAS_NO_ON_DISK = 1006;
public static final int INVALID_KEY = 1007;
public final int code;
public ServiceErrorException(int code, @Nullable String message, Throwable throwable) {
super(message, throwable);
this.code = code;
}
public ServiceErrorException(int code, @Nullable String message) {
this(code, message, null);
}
public ServiceErrorException(int code) {
this(code, null);
}
}

@ -0,0 +1,308 @@
package com.wallet.crypto.trustapp.util;
import android.app.Activity;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.Intent;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.security.keystore.UserNotAuthenticatedException;
import android.util.Log;
import com.wallet.crypto.trustapp.R;
import com.wallet.crypto.trustapp.controller.ServiceErrorException;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import static com.wallet.crypto.trustapp.controller.ServiceErrorException.INVALID_KEY;
import static com.wallet.crypto.trustapp.controller.ServiceErrorException.IV_OR_ALIAS_NO_ON_DISK;
import static com.wallet.crypto.trustapp.controller.ServiceErrorException.KEY_IS_GONE;
import static com.wallet.crypto.trustapp.controller.ServiceErrorException.KEY_STORE_ERROR;
import static com.wallet.crypto.trustapp.controller.ServiceErrorException.USER_NOT_AUTHENTICATED;
public class KS {
private static final String TAG = "KS";
private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
private static final String BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC;
private static final int AUTH_DURATION_SEC = 600;
private static final String PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7;
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS7Padding";
private synchronized static boolean setData(
Context context,
byte[] data,
String alias,
String aliasFile,
String aliasIV) throws ServiceErrorException {
if (data == null) {
throw new ServiceErrorException(
ServiceErrorException.INVALID_DATA, "keystore insert data is null");
}
KeyStore keyStore;
try {
keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
keyStore.load(null);
// Create the keys if necessary
if (!keyStore.containsAlias(alias)) {
KeyGenerator keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
ANDROID_KEY_STORE);
// Set the alias of the entry in Android KeyStore where the key will appear
// and the constrains (purposes) in the constructor of the Builder
keyGenerator.init(new KeyGenParameterSpec.Builder(
alias,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(BLOCK_MODE)
.setKeySize(256)
.setUserAuthenticationRequired(true)
.setUserAuthenticationValidityDurationSeconds(AUTH_DURATION_SEC)
.setRandomizedEncryptionRequired(true)
.setEncryptionPaddings(PADDING)
.build());
keyGenerator.generateKey();
}
String encryptedDataFilePath = getFilePath(context, aliasFile);
SecretKey secret = (SecretKey) keyStore.getKey(alias, null);
if (secret == null) {
throw new ServiceErrorException(
ServiceErrorException.KEY_STORE_SECRET,
"secret is null on setData: " + alias);
}
Cipher inCipher = Cipher.getInstance(CIPHER_ALGORITHM);
inCipher.init(Cipher.ENCRYPT_MODE, secret);
byte[] iv = inCipher.getIV();
String path = getFilePath(context, aliasIV);
boolean success = writeBytesToFile(path, iv);
if (!success) {
keyStore.deleteEntry(alias);
throw new ServiceErrorException(
ServiceErrorException.FAIL_TO_SAVE_IV_FILE,
"Failed to save the iv file for: " + alias);
}
CipherOutputStream cipherOutputStream = null;
try {
cipherOutputStream = new CipherOutputStream(
new FileOutputStream(encryptedDataFilePath),
inCipher);
cipherOutputStream.write(data);
} catch (Exception ex) {
throw new ServiceErrorException(
ServiceErrorException.KEY_STORE_ERROR,
"Failed to save the file for: " + alias);
} finally {
if (cipherOutputStream != null) {
cipherOutputStream.close();
}
}
return true;
} catch (UserNotAuthenticatedException e) {
throw new ServiceErrorException(USER_NOT_AUTHENTICATED);
} catch (ServiceErrorException ex) {
Log.d(TAG, "Key store error", ex);
throw ex;
} catch (Exception ex) {
Log.d(TAG, "Key store error", ex);
throw new ServiceErrorException(KEY_STORE_ERROR);
}
}
private synchronized static byte[] getData(
final Context context,
String alias,
String aliasFile,
String aliasIV)
throws ServiceErrorException {
KeyStore keyStore;
String encryptedDataFilePath = getFilePath(context, aliasFile);
try {
keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
keyStore.load(null);
SecretKey secretKey = (SecretKey) keyStore.getKey(alias, null);
if (secretKey == null) {
/* no such key, the key is just simply not there */
boolean fileExists = new File(encryptedDataFilePath).exists();
if (!fileExists) {
return null;/* file also not there, fine then */
}
throw new ServiceErrorException(
KEY_IS_GONE,
"file is present but the key is gone: " + alias);
}
boolean ivExists = new File(getFilePath(context, aliasIV)).exists();
boolean aliasExists = new File(getFilePath(context, aliasFile)).exists();
if (!ivExists || !aliasExists) {
removeAliasAndFiles(context, alias, aliasFile, aliasIV);
//report it if one exists and not the other.
if (ivExists != aliasExists) {
throw new ServiceErrorException(
IV_OR_ALIAS_NO_ON_DISK,
"file is present but the key is gone: " + alias);
} else {
throw new ServiceErrorException(
IV_OR_ALIAS_NO_ON_DISK,
"!ivExists && !aliasExists: " + alias);
}
}
byte[] iv = readBytesFromFile(getFilePath(context, aliasIV));
if (iv == null || iv.length == 0) {
throw new NullPointerException("iv is missing for " + alias);
}
Cipher outCipher = Cipher.getInstance(CIPHER_ALGORITHM);
outCipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
CipherInputStream cipherInputStream = new CipherInputStream(new FileInputStream(encryptedDataFilePath), outCipher);
return readBytesFromStream(cipherInputStream);
} catch (InvalidKeyException e) {
if (e instanceof UserNotAuthenticatedException) {
// showAuthenticationScreen(context, requestCode);
throw new ServiceErrorException(USER_NOT_AUTHENTICATED);
} else {
throw new ServiceErrorException(INVALID_KEY);
}
} catch (IOException | CertificateException | KeyStoreException | UnrecoverableKeyException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
throw new ServiceErrorException(KEY_STORE_ERROR);
}
}
private synchronized static String getFilePath(Context context, String fileName) {
return new File(context.getFilesDir(), fileName).getAbsolutePath();
}
private static boolean writeBytesToFile(String path, byte[] data) {
FileOutputStream fos = null;
try {
File file = new File(path);
fos = new FileOutputStream(file);
// Writes bytes from the specified byte array to this file output stream
fos.write(data);
return true;
} catch (FileNotFoundException e) {
System.out.println("File not found" + e);
} catch (IOException ioe) {
System.out.println("Exception while writing file " + ioe);
} finally {
// close the streams using close method
try {
if (fos != null) {
fos.close();
}
} catch (IOException ioe) {
System.out.println("Error while closing stream: " + ioe);
}
}
return false;
}
public synchronized static void removeAliasAndFiles(Context context, String alias, String dataFileName, String ivFileName) {
KeyStore keyStore;
try {
keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
keyStore.load(null);
keyStore.deleteEntry(alias);
new File(getFilePath(context, dataFileName)).delete();
new File(getFilePath(context, ivFileName)).delete();
} catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) {
e.printStackTrace();
}
}
public static byte[] readBytesFromStream(InputStream in) {
// this dynamically extends to take the bytes you read
ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream();
// this is storage overwritten on each iteration with bytes
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
// we need to know how may bytes were read to write them to the byteBuffer
int len;
try {
while ((len = in.read(buffer)) != -1) {
byteBuffer.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
byteBuffer.close();
} catch (IOException e) {
e.printStackTrace();
}
if (in != null) try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// and then we can return your byte array.
return byteBuffer.toByteArray();
}
private static byte[] readBytesFromFile(String path) {
byte[] bytes = null;
FileInputStream fin;
try {
File file = new File(path);
fin = new FileInputStream(file);
bytes = readBytesFromStream(fin);
} catch (IOException e) {
e.printStackTrace();
}
return bytes;
}
public static boolean put(Context context, String address, String password) throws ServiceErrorException {
return setData(context, password.getBytes(), address, address, address+"iv");
}
public static byte[] get(Context context, String address) throws ServiceErrorException {
return getData(context, address, address, address+"iv");
}
public static void showAuthenticationScreen(Context context, int requestCode) {
// Create the Confirm Credentials screen. You can customize the title and description. Or
// we will provide a generic one for you if you leave it null
Log.e(TAG, "showAuthenticationScreen: ");
if (context instanceof Activity) {
Activity app = (Activity) context;
KeyguardManager mKeyguardManager = (KeyguardManager) app.getSystemService(Context.KEYGUARD_SERVICE);
if (mKeyguardManager == null) {
return;
}
Intent intent = mKeyguardManager
.createConfirmDeviceCredentialIntent(
context.getString(R.string.unlock_screen_title_android),
context.getString(R.string.unlock_screen_prompt_android));
if (intent != null) {
app.startActivityForResult(intent, requestCode);
} else {
Log.e(TAG, "showAuthenticationScreen: failed to create intent for auth");
app.finish();
}
} else {
Log.e(TAG, "showAuthenticationScreen: context is not activity!");
}
}
}

@ -0,0 +1,31 @@
package com.wallet.crypto.trustapp.util;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import com.wallet.crypto.trustapp.controller.ServiceErrorException;
import com.wallet.pwd.trustapp.PasswordManager;
import java.util.Map;
public class PMMigrateHelper {
public static void migrate(Context context) throws ServiceErrorException {
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context);
Map<String, ?> passwords = pref.getAll();
for (String key : passwords.keySet()) {
if (key.contains("-pwd")) {
String address = key.replace("-pwd", "");
try {
KS.put(context, address.toLowerCase(), PasswordManager.getPassword(address, context));
pref.edit().remove(key).apply();
} catch (ServiceErrorException ex) {
throw ex;
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
}

@ -1,10 +1,9 @@
package com.wallet.crypto.trustapp.views;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
@ -13,6 +12,8 @@ import android.widget.Toast;
import com.wallet.crypto.trustapp.R;
import com.wallet.crypto.trustapp.controller.Controller;
import com.wallet.crypto.trustapp.controller.ServiceErrorException;
import com.wallet.crypto.trustapp.util.KS;
/**
* A login screen that offers login via email/password.
@ -48,13 +49,7 @@ public class CreateAccountActivity extends AppCompatActivity {
mCreateAccountButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
final String generatedPassphrase = Controller.generatePassphrase();
try {
mController.clickCreateAccount(CreateAccountActivity.this, "account name", generatedPassphrase);
} catch (Exception e) {
Toast.makeText(getApplicationContext(), "Create account: " + e.toString(), Toast.LENGTH_LONG).show();
}
onCreateAccount();
}
});
@ -67,7 +62,20 @@ public class CreateAccountActivity extends AppCompatActivity {
});
}
private void showIntro() {
private void onCreateAccount() {
final String generatedPassphrase = Controller.generatePassphrase();
try {
mController.clickCreateAccount(CreateAccountActivity.this, "account name", generatedPassphrase);
} catch (Exception e) {
if (e instanceof ServiceErrorException) {
KS.showAuthenticationScreen(CreateAccountActivity.this, Controller.UNLOCK_SCREEN_REQUEST);
} else {
Toast.makeText(getApplicationContext(), "Create account: " + e.toString(), Toast.LENGTH_LONG).show();
}
}
}
private void showIntro() {
// Declare a new thread to do a preference check
Thread t = new Thread(new Runnable() {
@Override
@ -105,6 +113,12 @@ public class CreateAccountActivity extends AppCompatActivity {
}
this.finish();
}
} else if (requestCode == Controller.UNLOCK_SCREEN_REQUEST) {
if (resultCode == RESULT_OK) {
onCreateAccount();
} else {
finish();
}
}
}

@ -18,6 +18,8 @@ import android.widget.Toast;
import com.wallet.crypto.trustapp.R;
import com.wallet.crypto.trustapp.controller.Controller;
import com.wallet.crypto.trustapp.controller.ServiceErrorException;
import com.wallet.crypto.trustapp.util.KS;
public class ExportAccountActivity extends AppCompatActivity {
@ -70,12 +72,19 @@ public class ExportAccountActivity extends AppCompatActivity {
return;
}
String keystoreJson = mController.clickExportAccount(ExportAccountActivity.this, mAddress, mPasswordText.getText().toString());
if (keystoreJson.isEmpty()) {
Toast.makeText(ExportAccountActivity.this, "Unable to export", Toast.LENGTH_SHORT).show();
} else {
mController.shareKeystore(ExportAccountActivity.this, keystoreJson);
}
String keystoreJson = null;
try {
keystoreJson = mController.clickExportAccount(ExportAccountActivity.this, mAddress, mPasswordText.getText().toString());
if (keystoreJson.isEmpty()) {
Toast.makeText(ExportAccountActivity.this, "Unable to export", Toast.LENGTH_SHORT).show();
} else {
mController.shareKeystore(ExportAccountActivity.this, keystoreJson);
}
} catch (ServiceErrorException e) {
if (e.code == ServiceErrorException.USER_NOT_AUTHENTICATED) {
KS.showAuthenticationScreen(ExportAccountActivity.this, Controller.UNLOCK_SCREEN_REQUEST);
}
}
}
});
}

@ -14,6 +14,7 @@ import com.wallet.crypto.trustapp.controller.Controller;
import com.wallet.crypto.trustapp.controller.OnTaskCompleted;
import com.wallet.crypto.trustapp.controller.TaskResult;
import com.wallet.crypto.trustapp.controller.TaskStatus;
import com.wallet.crypto.trustapp.util.KS;
import static android.app.Activity.RESULT_OK;
@ -60,7 +61,11 @@ public class ImportKeystoreFragment extends Fragment {
getActivity().setResult(RESULT_OK);
getActivity().finish();
} else {
Toast.makeText(getActivity(), result.getMessage(), Toast.LENGTH_SHORT).show();
if ("USER_NOT_AUTHENTICATED".equalsIgnoreCase(result.getMessage())) {
KS.showAuthenticationScreen(getContext(), Controller.UNLOCK_SCREEN_REQUEST);
} else {
Toast.makeText(getActivity(), result.getMessage(), Toast.LENGTH_SHORT).show();
}
}
}
});

@ -1,10 +1,12 @@
package com.wallet.crypto.trustapp.views;
import android.app.AlertDialog;
import android.content.Intent;
import android.graphics.Point;
import android.os.Bundle;
import android.os.Handler;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
@ -14,27 +16,24 @@ import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.gms.common.api.CommonStatusCodes;
import com.google.android.gms.vision.barcode.Barcode;
import com.wallet.crypto.trustapp.R;
import com.wallet.crypto.trustapp.controller.Controller;
import com.wallet.crypto.trustapp.controller.OnTaskCompleted;
import com.wallet.crypto.trustapp.controller.ServiceErrorException;
import com.wallet.crypto.trustapp.controller.TaskResult;
import com.wallet.crypto.trustapp.controller.TaskStatus;
import com.wallet.crypto.trustapp.controller.Utils;
import com.wallet.crypto.trustapp.model.VMAccount;
import com.wallet.crypto.trustapp.util.KS;
import com.wallet.crypto.trustapp.views.barcode.BarcodeCaptureActivity;
import com.google.android.gms.common.api.CommonStatusCodes;
import com.google.android.gms.vision.barcode.Barcode;
import org.ethereum.geth.Address;
import java.util.List;
public class SendActivity extends AppCompatActivity {
private Controller mController;
private EditText mTo;
private EditText mAmount;
private static final String LOG_TAG = SendActivity.class.getSimpleName();
private static final int BARCODE_READER_REQUEST_CODE = 1;
@ -43,14 +42,20 @@ public class SendActivity extends AppCompatActivity {
public static final String EXTRA_SYMBOL = "extra_symbol";
public static final String EXTRA_DECIMALS = "extra_decimals";
private final Handler handler = new Handler();
private EditText mTo;
private EditText mAmount;
private TextView mResultTextView;
private boolean mSendingTokens = false;
private String mContractAddress;
private String mSymbol;
private int mDecimals;
private AlertDialog dialog;
private boolean mSendingTokens = false;
private String mContractAddress;
private String mSymbol;
private int mDecimals;
@Override
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_send);
@ -59,104 +64,34 @@ public class SendActivity extends AppCompatActivity {
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
}
mController = Controller.with(this);
List<VMAccount> accounts = mController.getAccounts();
mTo = findViewById(R.id.date);
mController = Controller.with(this);
mTo = findViewById(R.id.date);
mAmount = findViewById(R.id.amount);
String toAddress = getIntent().getStringExtra(getString(R.string.address_keyword));
if (toAddress != null) {
mTo.setText(toAddress);
}
mContractAddress = getIntent().getStringExtra(EXTRA_CONTRACT_ADDRESS);
mDecimals = getIntent().getIntExtra(EXTRA_DECIMALS, -1);
mSymbol = getIntent().getStringExtra(EXTRA_SYMBOL);
mSendingTokens = getIntent().getBooleanExtra(EXTRA_SENDING_TOKENS, false);
assert(!mSendingTokens || (mSendingTokens && mDecimals > -1 && mContractAddress != null));
EditText amountView = findViewById(R.id.amount);
if (mSendingTokens && mSymbol != null) {
amountView.setHint(mSymbol + " amount");
mAmount.setHint(mSymbol + " amount");
} else {
amountView.setHint("ETH amount");
mAmount.setHint("ETH amount");
}
Button mSendButton = (Button) findViewById(R.id.send_button);
Button mSendButton = findViewById(R.id.send_button);
mSendButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// Validate input fields
boolean inputValid = true;
final String to = mTo.getText().toString();
if (!isAddressValid(to)) {
mTo.setError("Invalid address");
inputValid = false;
}
final String amount = mAmount.getText().toString();
if (!isValidEthAmount(amount)) {
mAmount.setError("Invalid amount");
inputValid = false;
}
if (!inputValid) {
return;
}
if (mSendingTokens) {
mController.clickSendTokens(
mController.getCurrentAccount().getAddress(),
mTo.getText().toString(),
mContractAddress,
mAmount.getText().toString(),
mDecimals,
new OnTaskCompleted() {
public void onTaskCompleted(final TaskResult result) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (result.getStatus() == TaskStatus.SUCCESS) {
SendActivity.this.finish();
}
Toast.makeText(SendActivity.this, result.getMessage(), Toast.LENGTH_LONG).show();
}
});
}
}
);
} else {
mController.clickSend(
mController.getCurrentAccount().getAddress(),
mTo.getText().toString(),
mAmount.getText().toString(),
new OnTaskCompleted() {
public void onTaskCompleted(final TaskResult result) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (result.getStatus() == TaskStatus.SUCCESS) {
SendActivity.this.finish();
}
Toast.makeText(SendActivity.this, result.getMessage(), Toast.LENGTH_LONG).show();
}
});
}
}
);
}
onSendClick();
}
});
mResultTextView = (TextView) findViewById(R.id.result_textview);
ImageButton scanBarcodeButton = (ImageButton) findViewById(R.id.scan_barcode_button);
mResultTextView = findViewById(R.id.result_textview);
ImageButton scanBarcodeButton = findViewById(R.id.scan_barcode_button);
scanBarcodeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
@ -166,7 +101,55 @@ public class SendActivity extends AppCompatActivity {
});
}
boolean isAddressValid(String address) {
@Override
protected void onPause() {
super.onPause();
hideSendProgress();
}
private void onSendClick() {
// Validate input fields
showSendProgress();
boolean inputValid = true;
final String to = mTo.getText().toString();
if (!isAddressValid(to)) {
mTo.setError("Invalid address");
inputValid = false;
}
final String amount = mAmount.getText().toString();
if (!isValidEthAmount(amount)) {
mAmount.setError("Invalid amount");
inputValid = false;
}
if (!inputValid) {
return;
}
try {
if (mSendingTokens) {
mController.clickSendTokens(
mController.getCurrentAccount().getAddress(),
mTo.getText().toString(),
mContractAddress,
mAmount.getText().toString(),
mDecimals,
onSendCompleteListener);
} else {
mController.clickSend(
mController.getCurrentAccount().getAddress(),
mTo.getText().toString(),
mAmount.getText().toString(),
onSendCompleteListener);
}
} catch (ServiceErrorException ex) {
hideSendProgress();
if (ex.code == ServiceErrorException.USER_NOT_AUTHENTICATED) {
KS.showAuthenticationScreen(this, Controller.UNLOCK_SCREEN_REQUEST);
}
}
}
boolean isAddressValid(String address) {
try {
new Address(address);
return true;
@ -187,21 +170,45 @@ public class SendActivity extends AppCompatActivity {
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == BARCODE_READER_REQUEST_CODE) {
if (resultCode == CommonStatusCodes.SUCCESS) {
if (data != null) {
Barcode barcode = data.getParcelableExtra(BarcodeCaptureActivity.BarcodeObject);
String extracted_address = Utils.extractAddressFromQrString(barcode.displayValue);
if (extracted_address == null) {
Toast.makeText(this, "QR code doesn't contain account address", Toast.LENGTH_SHORT).show();
return;
}
Point[] p = barcode.cornerPoints;
mTo.setText(extracted_address);
} else mResultTextView.setText(R.string.no_barcode_captured);
} else Log.e(LOG_TAG, String.format(getString(R.string.barcode_error_format),
CommonStatusCodes.getStatusCodeString(resultCode)));
} else super.onActivityResult(requestCode, resultCode, data);
if (resultCode == CommonStatusCodes.SUCCESS) {
if (data != null) {
Barcode barcode = data.getParcelableExtra(BarcodeCaptureActivity.BarcodeObject);
String extracted_address = Utils.extractAddressFromQrString(barcode.displayValue);
if (extracted_address == null) {
Toast.makeText(this, "QR code doesn't contain account address", Toast.LENGTH_SHORT).show();
return;
}
Point[] p = barcode.cornerPoints;
mTo.setText(extracted_address);
} else {
mResultTextView.setText(R.string.no_barcode_captured);
}
} else {
Log.e(LOG_TAG, String.format(getString(R.string.barcode_error_format),
CommonStatusCodes.getStatusCodeString(resultCode)));
}
} else if (requestCode == Controller.UNLOCK_SCREEN_REQUEST) {
if (resultCode == RESULT_OK) {
onSendClick();
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
private void showSendProgress() {
dialog = new AlertDialog.Builder(this)
.setView(R.layout.layout_send_progress)
.create();
dialog.show();
}
private void hideSendProgress() {
if (dialog != null && dialog.isShowing()) {
dialog.dismiss();
dialog = null;
}
}
@Override
@ -213,4 +220,29 @@ public class SendActivity extends AppCompatActivity {
}
return super.onOptionsItemSelected(item);
}
private final OnTaskCompleted onSendCompleteListener = new OnTaskCompleted() {
@Override
public void onTaskCompleted(TaskResult result) {
handler.post(new OnSendCompleteNotifier(result));
}
};
private class OnSendCompleteNotifier implements Runnable {
private final TaskResult result;
OnSendCompleteNotifier(TaskResult result) {
this.result = result;
}
@Override
public void run() {
if (result.getStatus() == TaskStatus.SUCCESS) {
finish();
}
hideSendProgress();
Toast.makeText(getApplicationContext(), result.getMessage(), Toast.LENGTH_LONG).show();
}
}
}

@ -14,7 +14,7 @@ public class SplashActivity extends AppCompatActivity {
// Start home activity
startActivity(new Intent(SplashActivity.this, TransactionListActivity.class));
startActivity(new Intent(this, TransactionListActivity.class));
// close splash activity

@ -1,6 +1,10 @@
package com.wallet.crypto.trustapp.views;
import android.app.AlertDialog;
import android.app.KeyguardManager;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
@ -16,14 +20,18 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import com.wallet.crypto.trustapp.R;
import com.wallet.crypto.trustapp.controller.Controller;
import com.wallet.crypto.trustapp.controller.OnTaskCompleted;
import com.wallet.crypto.trustapp.controller.ServiceErrorException;
import com.wallet.crypto.trustapp.controller.TaskResult;
import com.wallet.crypto.trustapp.model.ESTransaction;
import com.wallet.crypto.trustapp.model.VMAccount;
import com.wallet.crypto.trustapp.util.KS;
import com.wallet.crypto.trustapp.util.PMMigrateHelper;
import java.util.List;
@ -168,12 +176,44 @@ public class TransactionListActivity extends AppCompatActivity {
init();
Log.d(TAG, "Number of accounts: " + mController.getNumberOfAccounts());
if (mController.getAccounts().size() == 0) {
Intent intent = new Intent(getApplicationContext(), CreateAccountActivity.class);
this.startActivityForResult(intent, Controller.IMPORT_ACCOUNT_REQUEST);
finish();
KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
if (keyguardManager != null && !keyguardManager.isDeviceSecure()) {
new AlertDialog.Builder(this)
.setTitle(R.string.lock_title)
.setMessage(R.string.lock_body)
.setPositiveButton(R.string.lock_settings, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD);
startActivity(intent);
}
})
.setNegativeButton(R.string.lock_exit, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
System.exit(0);
}
})
.show();
} else {
mController.onResume();
if (mController.getAccounts().size() == 0) {
Intent intent = new Intent(getApplicationContext(), CreateAccountActivity.class);
this.startActivityForResult(intent, Controller.IMPORT_ACCOUNT_REQUEST);
finish();
} else {
mController.onResume();
try {
PMMigrateHelper.migrate(this);
} catch (ServiceErrorException e) {
if (e.code == ServiceErrorException.USER_NOT_AUTHENTICATED) {
KS.showAuthenticationScreen(this, Controller.UNLOCK_SCREEN_REQUEST);
} else {
Toast.makeText(this, "Could not process passwords.", Toast.LENGTH_LONG)
.show();
}
}
}
}
}
@ -182,6 +222,10 @@ public class TransactionListActivity extends AppCompatActivity {
if (resultCode == RESULT_OK) {
this.finish();
}
} else if (requestCode == Controller.UNLOCK_SCREEN_REQUEST) {
if (resultCode == RESULT_OK) {
}
}
}

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:padding="32dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<LinearLayout
android:layout_gravity="center"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
>
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<TextView
android:text="@string/sending_progress"
android:layout_gravity="center_vertical"
android:layout_marginLeft="8dp"
android:layout_marginEnd="8dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</LinearLayout>
</FrameLayout>

@ -108,5 +108,12 @@
<string name="tab_keystore">Keystore</string>
<string name="tab_private_key">Private key</string>
<string name="action_transaction_detail">More details</string>
<string name="lock_title">Lock screen</string>
<string name="lock_body">Secure lock screen hasn\'t set up. To continue working with this app, please go to Settings and set a lock screen method.</string>
<string name="lock_settings">Lock settings</string>
<string name="lock_exit">Exit</string>
<string name="unlock_screen_title_android">Unlock screen</string>
<string name="unlock_screen_prompt_android">Please unlock your Android device to continue.</string>
<string name="sending_progress">Sending...</string>
</resources>

@ -5,7 +5,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.3'
classpath 'com.android.tools.build:gradle:3.0.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

Loading…
Cancel
Save