/* * Aegis Bitcoin Wallet - The secure Bitcoin wallet for Android * Copyright 2014 Bojan Simic and specularX.co, designed by Reuven Yamrom * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.aegiswallet.utils; import android.content.Context; import android.content.SharedPreferences; import android.text.format.DateUtils; import android.util.Base64; import android.util.Log; import com.aegiswallet.helpers.Crypto; import com.aegiswallet.helpers.secretshare.SecretShare; import com.google.bitcoin.core.Address; import com.google.bitcoin.core.AddressFormatException; import com.google.bitcoin.core.DumpedPrivateKey; import com.google.bitcoin.core.ECKey; import com.google.bitcoin.core.ScriptException; import com.google.bitcoin.core.Transaction; import com.google.bitcoin.core.TransactionInput; import com.google.bitcoin.core.TransactionOutput; import com.google.bitcoin.core.Wallet; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.io.Writer; import java.math.BigDecimal; import java.math.BigInteger; import java.math.RoundingMode; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.crypto.SecretKey; /** * Created by bsimic on 3/10/14. */ public class WalletUtils { private static final String TAG = WalletUtils.TAG; private static int passwordIterations = 10000; private static int keySize = 256; public static final BigInteger ONE_BTC = new BigInteger("100000000", 10); public static final BigInteger ONE_MBTC = new BigInteger("100000", 10); public static void writeEncryptedKeys(@Nonnull final Writer out, @Nonnull final List<ECKey> keys, SharedPreferences prefs, String passOrNFC) throws IOException { boolean nfcEnabled = prefs.contains(Constants.SHAMIR_ENCRYPTED_KEY) ? false : true; String x1 = prefs.getString(Constants.SHAMIR_LOCAL_KEY, null); String x2 = null; String encodedEncryptedX2 = null; if (!nfcEnabled) { x2 = prefs.getString(Constants.SHAMIR_ENCRYPTED_KEY, null); String encryptedX2 = encryptString(x2, passOrNFC); encodedEncryptedX2 = Base64.encodeToString(encryptedX2.getBytes("UTF-8"), Base64.NO_WRAP); } out.write("# PRIVATE KEYS ARE ENCRYPTED WITH SHAMIR SECRET SHARING\n"); out.write("# TO DECRYPT - Import this backup and provide your password or NFC token\n"); out.write("# If password/NFC token are lost, contact Bitcoin Security Project. We may be able to help.\n"); out.write("#" + x1); out.write("\n"); if (!nfcEnabled && encodedEncryptedX2 != null) { out.write("#X2:" + encodedEncryptedX2); out.write("\n"); out.write("#ENCTYPE:PASSWORD"); } //Means NFC is enabled and we're using that for encryption else if (nfcEnabled) { out.write("#ENCTYPE:NFC"); } out.write("\n"); BigInteger mainKey = null; if (nfcEnabled) { mainKey = generateSecretFromStrings(x1, passOrNFC, null); } else if (x2 != null) { mainKey = generateSecretFromStrings(x1, x2, null); } String mainKeyHash = convertToSha256(mainKey.toString()); for (final ECKey key : keys) { String encodedKey = key.getPrivateKeyEncoded(Constants.NETWORK_PARAMETERS).toString(); String encryptedKey = encryptString(encodedKey, mainKeyHash); out.write(Base64.encodeToString(encryptedKey.getBytes(), Base64.NO_WRAP)); out.write('\n'); } } public static boolean checkFileBackupNFCEncrypted(String fileName) { boolean result = false; try { if (Constants.WALLET_BACKUP_DIRECTORY.exists() && Constants.WALLET_BACKUP_DIRECTORY.isDirectory()) { File file = new File(Constants.WALLET_BACKUP_DIRECTORY, fileName); FileInputStream fileInputStream = new FileInputStream(file); final BufferedReader in = new BufferedReader(new InputStreamReader(fileInputStream, Constants.UTF_8)); while (true) { final String line = in.readLine(); if (line == null) break; // eof if (line.startsWith("#ENCTYPE:NFC")) { return true; } } } } catch (FileNotFoundException e) { Log.e(TAG, "File not found: " + e.getMessage()); } catch (IOException e) { Log.e(TAG, "IO Exception: " + e.getMessage()); } catch (Exception e) { Log.e(TAG, "some other exception: " + e.getMessage()); } return result; } /** * This file will check the password provided when importing a backup file. If the password is correct, * it will return true, if not, false. * * @param fileName * @param password * @return */ public static boolean checkPasswordForBackupFile(String fileName, String password) { boolean result = false; try { if (Constants.WALLET_BACKUP_DIRECTORY.exists() && Constants.WALLET_BACKUP_DIRECTORY.isDirectory()) { File file = new File(Constants.WALLET_BACKUP_DIRECTORY, fileName); FileInputStream fileInputStream = new FileInputStream(file); final BufferedReader in = new BufferedReader(new InputStreamReader(fileInputStream, Constants.UTF_8)); while (true) { final String line = in.readLine(); if (line == null) break; // eof if (line.startsWith("#X2:")) { String[] splitStr = line.split(":"); String x2Base64 = splitStr[1]; String x2Encrypted = new String(Base64.decode(x2Base64.getBytes(), Base64.NO_WRAP)); String x2Decrypted = WalletUtils.decryptString(x2Encrypted, password); x2Decrypted = x2Decrypted.split(":")[1]; if (x2Decrypted != null) { return true; } } } } } catch (FileNotFoundException e) { Log.e(TAG, "File not found: " + e.getMessage()); } catch (IOException e) { Log.e(TAG, "IO Exception: " + e.getMessage()); } catch (Exception e) { Log.e(TAG, "some other exception: " + e.getMessage()); } return result; } public static List<ECKey> restoreWalletFromBackupFile(String fileName, String passOrNFC, Wallet wallet, boolean shouldAddKeys) { final List<ECKey> keys = new LinkedList<ECKey>(); boolean nfcEncrypted; try { if (Constants.WALLET_BACKUP_DIRECTORY.exists() && Constants.WALLET_BACKUP_DIRECTORY.isDirectory()) { File file = new File(Constants.WALLET_BACKUP_DIRECTORY, fileName); FileInputStream fileInputStream = new FileInputStream(file); final BufferedReader in = new BufferedReader(new InputStreamReader(fileInputStream, Constants.UTF_8)); String x1 = null; String x2Encrypted = null; String x2Decrypted = null; String secretString = null; while (true) { final String line = in.readLine(); if (line == null) break; // eof if (line.startsWith("# ")) continue; if (line.trim().isEmpty()) continue; if (line.startsWith("#1:")) { String[] splitStr = line.split(":"); x1 = splitStr[1]; continue; } if (line.startsWith("#X2:")) { String[] splitStr = line.split(":"); String x2Base64 = splitStr[1]; x2Encrypted = new String(Base64.decode(x2Base64.getBytes(), Base64.NO_WRAP)); x2Decrypted = WalletUtils.decryptString(x2Encrypted, passOrNFC); x2Decrypted = x2Decrypted.split(":")[1]; BigInteger secret = WalletUtils.generateSecretFromStrings("1:" + x1, "2:" + x2Decrypted, null); secretString = WalletUtils.convertToSha256(secret.toString()); continue; } if (line.startsWith("#ENCTYPE:NFC")) { x2Decrypted = passOrNFC; BigInteger secret = WalletUtils.generateSecretFromStrings("1:" + x1, x2Decrypted, null); secretString = WalletUtils.convertToSha256(secret.toString()); continue; } if (line.startsWith("#ENCTYPE:PASSWORD")) continue; String encryptedKey = new String(Base64.decode(line.getBytes(), Base64.NO_WRAP)); String plainKey = WalletUtils.decryptString(encryptedKey, secretString); ECKey key = new DumpedPrivateKey(Constants.NETWORK_PARAMETERS, plainKey).getKey(); if (!wallet.hasKey(key)) keys.add(key); } //Only add keys if the parameter says so. This is because the wallet may be still encrypted. //We dont want to add keys to an encrypted wallet. That's bad. if (shouldAddKeys) wallet.addKeys(keys); } } catch (final AddressFormatException x) { Log.e(TAG, "exception caught: " + x.getMessage()); } catch (IOException e) { Log.e(TAG, "exception caught: " + e.getMessage()); } catch (Exception e) { Log.e(TAG, "exception caught: " + e.getMessage()); } return keys; } public static List<ECKey> readKeys(@Nonnull final BufferedReader in) throws IOException { try { final DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); final List<ECKey> keys = new LinkedList<ECKey>(); while (true) { final String line = in.readLine(); if (line == null) break; // eof if (line.trim().isEmpty() || line.charAt(0) == '#') continue; // skip comment final String[] parts = line.split(" "); final ECKey key = new DumpedPrivateKey(Constants.NETWORK_PARAMETERS, parts[0]).getKey(); key.setCreationTimeSeconds(parts.length >= 2 ? format.parse(parts[1]).getTime() / DateUtils.SECOND_IN_MILLIS : 0); keys.add(key); } return keys; } catch (final AddressFormatException x) { throw new IOException("cannot read keys", x); } catch (final ParseException x) { throw new IOException("cannot read keys", x); } } public static Address getFirstFromAddress(@Nonnull final Transaction transaction) { if (transaction.isCoinBase()) return null; try { for (final TransactionInput input : transaction.getInputs()) { return input.getFromAddress(); } throw new IllegalStateException(); } catch (final ScriptException x) { return null; } } @CheckForNull public static Address getFirstToAddress(@Nonnull final Transaction transaction) { try { for (final TransactionOutput output : transaction.getOutputs()) { return output.getScriptPubKey().getToAddress(Constants.NETWORK_PARAMETERS); } throw new IllegalStateException(); } catch (final ScriptException x) { return null; } } public static String getBTCCurrencryValue(Context context, SharedPreferences prefs, BigDecimal amount) { String result = ""; File file = context.getApplicationContext().getFileStreamPath(Constants.BLOCKCHAIN_CURRENCY_FILE_NAME); if (file.exists()) { JSONObject jsonObject = BasicUtils.parseJSONData(context, Constants.BLOCKCHAIN_CURRENCY_FILE_NAME); try { if (jsonObject != null) { JSONObject newObject = jsonObject.getJSONObject(prefs.getString(Constants.CURRENCY_PREF_KEY, null)); Double doubleVal = newObject.getDouble("last"); BigDecimal decimal = BigDecimal.valueOf(doubleVal); result = newObject.getString("symbol") + decimal.multiply(amount).setScale(2, RoundingMode.HALF_EVEN).toString(); } } catch (JSONException e) { Log.e("Wallet Utils", "JSON Exception " + e.getMessage()); } } return result; } public static String getWalletCurrencyValue(Context context, SharedPreferences prefs, BigInteger balance) { String result = ""; File file = context.getApplicationContext().getFileStreamPath(Constants.BLOCKCHAIN_CURRENCY_FILE_NAME); if (file.exists()) { JSONObject jsonObject = BasicUtils.parseJSONData(context, Constants.BLOCKCHAIN_CURRENCY_FILE_NAME); try { String balanceInBTC = balance.toString(); if (balance.longValue() > 0) balanceInBTC = BasicUtils.formatValue(balance, Constants.BTC_MAX_PRECISION, 0); BigDecimal formattedBalance = new BigDecimal(balanceInBTC); if (jsonObject != null) { JSONObject newObject = jsonObject.getJSONObject(prefs.getString(Constants.CURRENCY_PREF_KEY, null)); Double doubleVal = newObject.getDouble("last"); BigDecimal decimal = BigDecimal.valueOf(doubleVal); result = newObject.getString("symbol") + decimal.multiply(formattedBalance).setScale(2, RoundingMode.HALF_EVEN).toString(); } } catch (JSONException e) { Log.e("Wallet Utils", "JSON Exception " + e.getMessage()); } } return result; } public static BigDecimal getExchangeRate(Context context, SharedPreferences prefs) { File file = context.getApplicationContext().getFileStreamPath(Constants.BLOCKCHAIN_CURRENCY_FILE_NAME); if (file.exists()) { JSONObject jsonObject = BasicUtils.parseJSONData(context, Constants.BLOCKCHAIN_CURRENCY_FILE_NAME); try { if (jsonObject != null) { JSONObject newObject = jsonObject.getJSONObject(prefs.getString(Constants.CURRENCY_PREF_KEY, null)); double doubleValue = newObject.getDouble("last"); BigDecimal bigDecimal = BigDecimal.valueOf(doubleValue); return bigDecimal; } } catch (JSONException e) { Log.e("Wallet Utils", "JSON Exception " + e.getMessage()); } } return null; } public static String getExchangeRateWithSymbol(Context context, SharedPreferences prefs) { File file = context.getApplicationContext().getFileStreamPath(Constants.BLOCKCHAIN_CURRENCY_FILE_NAME); if (file.exists()) { JSONObject jsonObject = BasicUtils.parseJSONData(context, Constants.BLOCKCHAIN_CURRENCY_FILE_NAME); try { if (jsonObject != null) { JSONObject newObject = jsonObject.getJSONObject(prefs.getString(Constants.CURRENCY_PREF_KEY, null)); double doubleValue = newObject.getDouble("last"); BigDecimal bigDecimal = BigDecimal.valueOf(doubleValue); return newObject.getString("symbol") + bigDecimal.toString(); } } catch (JSONException e) { Log.e("Wallet Utils", "JSON Exception " + e.getMessage()); } } return null; } public static BigInteger btcValue(Context context, SharedPreferences prefs, @Nonnull final BigInteger localValue) { BigInteger result = null; BigDecimal exchangeRate = getExchangeRate(context, prefs); if (exchangeRate != null) result = localValue.multiply(ONE_BTC).divide(exchangeRate.toBigInteger()); return result; } public static String encryptString(String plainText, String password) { byte[] salt = Crypto.generateSalt(); SecretKey key = Crypto.deriveKeyPbkdf2(salt, password); return Crypto.encrypt(plainText, key, salt); } public static String decryptString(String ciphertext, String password) { return Crypto.decryptPbkdf2(ciphertext, password); } private static String getRawKey(SecretKey key) { if (key == null) { return null; } return Crypto.toHex(key.getEncoded()); } public static String generateSalt() { SecureRandom random = new SecureRandom(); byte bytes[] = new byte[keySize / 8]; random.nextBytes(bytes); String s = new String(bytes); return s; } public static String convertToSha256(String plainText) { String result = null; try { MessageDigest md = MessageDigest.getInstance("SHA-256"); md.update(plainText.getBytes()); byte byteData[] = md.digest(); StringBuffer sb = new StringBuffer(); for (int i = 0; i < byteData.length; i++) { sb.append(Integer.toString((byteData[i] & 0xff) + 0x100, 16).substring(1)); } StringBuffer hexString = new StringBuffer(); for (int i = 0; i < byteData.length; i++) { String hex = Integer.toHexString(0xff & byteData[i]); if (hex.length() == 1) hexString.append('0'); hexString.append(hex); } result = hexString.toString(); } catch (NoSuchAlgorithmException e) { Log.e(TAG, e.getMessage()); } finally { return result; } } public static BigInteger generateSecretFromStrings(String x1String, String x2String, String x3String) { List<SecretShare.ShareInfo> newShares = new ArrayList<SecretShare.ShareInfo>(); SecretShare.PublicInfo publicInfo1 = new SecretShare.PublicInfo(3, 2, null, null); SecretShare.PublicInfo publicInfo2 = new SecretShare.PublicInfo(3, 2, null, null); SecretShare.PublicInfo publicInfo3 = new SecretShare.PublicInfo(3, 2, null, null); SecretShare.ShareInfo shareInfoOne; SecretShare.ShareInfo shareInfoTwo; SecretShare.ShareInfo shareInfoThree; if (x1String != null) { BigInteger one = new BigInteger(getShareOrX(x1String, true)); int x1 = new Integer(getShareOrX(x1String, false)).intValue(); shareInfoOne = new SecretShare.ShareInfo(x1, one, publicInfo1); newShares.add(shareInfoOne); } if (x2String != null) { BigInteger two = new BigInteger(getShareOrX(x2String, true)); int x2 = new Integer(getShareOrX(x2String, false)).intValue(); shareInfoTwo = new SecretShare.ShareInfo(x2, two, publicInfo2); newShares.add(shareInfoTwo); } if (x3String != null) { BigInteger three = new BigInteger(getShareOrX(x3String, true)); int x3 = new Integer(getShareOrX(x3String, false)).intValue(); shareInfoThree = new SecretShare.ShareInfo(x3, three, publicInfo3); newShares.add(shareInfoThree); } if (newShares.size() >= 2) { SecretShare.PublicInfo publicInfo = newShares.get(0).getPublicInfo(); SecretShare solver = new SecretShare(publicInfo); SecretShare.CombineOutput solved = solver.combine(newShares); return solved.getSecret(); } return null; } public static String getShareOrX(String x, boolean getShare) { if (x != null) { if (!getShare && x.contains(":")) return x.split(":")[0]; else if (x.contains(":")) return x.split(":")[1]; } return null; } public static boolean checkPassword(String password, SharedPreferences prefs) { boolean result = false; String passwordSalt = prefs.getString(Constants.PASSWORD_SALT, ""); String passwordHash = passwordSalt + convertToSha256(passwordSalt + password); String storedHash = prefs.getString(Constants.PASSWORD_HASH, ""); if (passwordHash.equals(storedHash)) result = true; return result; } public static void changePassword(String password, SharedPreferences prefs) { String passwordSalt = BasicUtils.generateSecureKey(); String passwordHash = passwordSalt + WalletUtils.convertToSha256(passwordSalt + password); prefs.edit().putString(Constants.PASSWORD_HASH, passwordHash).commit(); prefs.edit().putString(Constants.PASSWORD_SALT, passwordSalt).commit(); prefs.edit().putBoolean(Constants.APP_INIT_COMPLETE, true).commit(); } public static boolean isTransactionRelevant(Transaction tx, Wallet wallet) throws ScriptException { return tx.getValueSentFromMe(wallet).compareTo(BigInteger.ZERO) > 0 || tx.getValueSentToMe(wallet).compareTo(BigInteger.ZERO) > 0 || tx.isPending(); } public static ArrayList<Transaction> getRelevantTransactions(ArrayList<Transaction> currentTxs, Wallet wallet){ ArrayList<Transaction> newList = new ArrayList<Transaction>(); if(currentTxs != null){ for(Transaction transaction : currentTxs){ if(WalletUtils.isTransactionRelevant(transaction, wallet)){ newList.add(transaction); } } } return newList; } public static boolean isAddressMine(Wallet w, Address a){ List<ECKey> keys = w.getKeys(); for (ECKey key : keys) { Address address = key.toAddress(Constants.NETWORK_PARAMETERS); if(a.toString().equals(address.toString())){ return true; } } return false; } }