/*
* Copyright 2011-2012 the original author or authors.
*
* 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 piuk.blockchain.android.util;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.text.Editable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.TypefaceSpan;
import android.util.Pair;
//import android.util.Log;
import com.google.bitcoin.core.Address;
import com.google.bitcoin.core.Base58;
import com.google.bitcoin.core.DumpedPrivateKey;
import com.google.bitcoin.core.ECKey;
import com.google.bitcoin.core.NetworkParameters;
import com.google.bitcoin.core.Utils;
import com.google.bitcoin.params.MainNetParams;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import piuk.blockchain.android.Hash;
import piuk.blockchain.android.MyWallet;
import piuk.blockchain.android.Constants;
import java.io.DataOutputStream;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.Map;
import java.util.Map.Entry;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ArrayUtils;
import org.spongycastle.asn1.sec.SECNamedCurves;
import org.spongycastle.crypto.generators.SCrypt;
import org.spongycastle.util.encoders.Hex;
/**
* @author Andreas Schildbach
*/
public class WalletUtils {
public final static QRCodeWriter QR_CODE_WRITER = new QRCodeWriter();
public static final int DefaultRequestRetry = 2;
public static final int DefaultRequestTimeout = 60000;
public static ECKey parsePrivateKey(String format, String contents, String password) throws Exception {
if (format.equals("sipa") || format.equals("compsipa")) {
DumpedPrivateKey pk = new DumpedPrivateKey(MainNetParams.get(), contents);
return pk.getKey();
} else if (format.equals("base58")) {
return MyWallet.decodeBase58PK(contents);
} else if (format.equals("base64")) {
return MyWallet.decodeBase64PK(contents);
} else if (format.equals("hex")) {
return MyWallet.decodeHexPK(contents);
}else if (format.equals("bip38")) {
return parseBIP38 (contents, password);
} else {
throw new Exception("Unable to handle format " + format);
}
}
public static String postURLWithParams(String request, Map<Object, Object> params) throws Exception {
StringBuffer urlParameters = new StringBuffer();
for (Entry<Object, Object> entry : params.entrySet()) {
urlParameters.append(entry.getKey() + "=" + URLEncoder.encode(entry.getValue().toString(), "UTF-8") + "&");
}
return postURL(request, urlParameters.toString());
}
public static String postURL(String request, String urlParameters) throws Exception {
String error = null;
for (int ii = 0; ii < DefaultRequestRetry; ++ii) {
URL url = new URL(request);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
try {
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setInstanceFollowRedirects(false);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
connection.setRequestProperty("charset", "utf-8");
connection.setRequestProperty("Accept", "application/json");
connection.setRequestProperty("Content-Length", "" + Integer.toString(urlParameters.getBytes().length));
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36");
connection.setUseCaches (false);
connection.setConnectTimeout(DefaultRequestTimeout);
connection.setReadTimeout(DefaultRequestTimeout);
connection.connect();
DataOutputStream wr = new DataOutputStream(connection.getOutputStream ());
wr.writeBytes(urlParameters);
wr.flush();
wr.close();
connection.setInstanceFollowRedirects(false);
if (connection.getResponseCode() == 200) {
// Log.d("postURL", "return code 200");
return IOUtils.toString(connection.getInputStream(), "UTF-8");
}
else {
error = IOUtils.toString(connection.getErrorStream(), "UTF-8");
// Log.d("postURL", "return code " + error);
}
Thread.sleep(5000);
} finally {
connection.disconnect();
}
}
throw new Exception("Inavlid Response " + error);
}
public static String getURL(String URL) throws Exception {
URL url = new URL(URL);
String error = null;
for (int ii = 0; ii < DefaultRequestRetry; ++ii) {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
try {
connection.setRequestMethod("GET");
connection.setRequestProperty("charset", "utf-8");
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36");
connection.setConnectTimeout(DefaultRequestTimeout);
connection.setReadTimeout(DefaultRequestTimeout);
connection.setInstanceFollowRedirects(false);
connection.connect();
if (connection.getResponseCode() == 200)
return IOUtils.toString(connection.getInputStream(), "UTF-8");
else
error = IOUtils.toString(connection.getErrorStream(), "UTF-8");
Thread.sleep(5000);
} finally {
connection.disconnect();
}
}
return error;
}
public static String detectPrivateKeyFormat(String key) throws Exception {
// 51 characters base58, always starts with a '5'
if (key.matches("^5[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{50}$"))
return "sipa";
if (key.matches("^[LK][123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{51}$"))
return "compsipa";
// 52 characters base58
if (key.matches("^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{44}$") || key.matches("^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43}$"))
return "base58";
if (key.matches("^[A-Fa-f0-9]{64}$"))
return "hex";
if (key.matches("^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789=+]{44}$"))
return "base64";
if (key.matches("^6P[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{56}$"))
return "bip38";
if (key.matches("^S[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21}$") ||
key.matches("^S[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{25}$") ||
key.matches("^S[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{29}$") ||
key.matches("^S[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{30}$")) {
byte[] testBytes = SHA256(key + "?").getBytes();
if (testBytes[0] == 0x00 || testBytes[0] == 0x01)
return "mini";
}
throw new Exception("Unknown Key Format");
}
public static String SHA256Hex(String str) {
try {
return new String(Hex.encode(MessageDigest.getInstance("SHA-256").digest(str.getBytes("UTF-8"))), "UTF-8");
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static Hash SHA256(String str) {
try {
return new Hash(MessageDigest.getInstance("SHA-256").digest(str.getBytes("UTF-8")));
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static Bitmap getQRCodeBitmap(final String url, final int size) {
try {
final Hashtable<EncodeHintType, Object> hints = new Hashtable<EncodeHintType, Object>();
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
hints.put(EncodeHintType.MARGIN, 2);
final BitMatrix result = QR_CODE_WRITER.encode(url,
BarcodeFormat.QR_CODE, size, size, hints);
final int width = result.getWidth();
final int height = result.getHeight();
final int[] pixels = new int[width * height];
for (int y = 0; y < height; y++) {
final int offset = y * width;
for (int x = 0; x < width; x++) {
pixels[offset + x] = result.get(x, y) ? Color.BLACK
: Color.TRANSPARENT;
}
}
final Bitmap bitmap = Bitmap.createBitmap(width, height,
Bitmap.Config.ARGB_8888);
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
return bitmap;
} catch (final WriterException x) {
x.printStackTrace();
return null;
}
}
public static Editable formatAddress(final Address address,
final int groupSize, final int lineSize) {
return formatAddress(address.toString(), groupSize, lineSize);
}
public static Editable formatAddress(final String address,
final int groupSize, final int lineSize) {
final SpannableStringBuilder builder = new SpannableStringBuilder();
final int len = address.length();
for (int i = 0; i < len; i += groupSize) {
final int end = i + groupSize;
final String part = address.substring(i, end < len ? end : len);
builder.append(part);
builder.setSpan(new TypefaceSpan("monospace"), builder.length()
- part.length(), builder.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
if (end < len) {
final boolean endOfLine = end % lineSize == 0;
builder.append(endOfLine ? "\n" : Constants.THIN_SPACE);
}
}
return builder;
}
public static String formatValue(final BigInteger value) {
return formatValue(value, "", "-");
}
public static String formatValue(final BigInteger value,
final String plusSign, final String minusSign) {
final boolean negative = value.compareTo(BigInteger.ZERO) < 0;
final BigInteger absValue = value.abs();
final String sign = negative ? minusSign : plusSign;
final int coins = absValue.divide(Utils.COIN).intValue();
final int cents = absValue.remainder(Utils.COIN).intValue();
if (cents % 1000000 == 0)
return String.format("%s%d.%02d", sign, coins, cents / 1000000);
else if (cents % 10000 == 0)
return String.format("%s%d.%04d", sign, coins, cents / 10000);
else
return String.format("%s%d.%08d", sign, coins, cents);
}
public static byte[] hash (byte[] data, int offset, int len)
{
try
{
MessageDigest a = MessageDigest.getInstance ("SHA-256");
a.update (data, offset, len);
return a.digest (a.digest ());
}
catch ( NoSuchAlgorithmException e )
{
throw new RuntimeException (e);
}
}
public static byte[] hash (byte[] data)
{
return hash (data, 0, data.length);
}
public static ECKey parseBIP38 (String input, String password) throws Exception
{
byte[] store = Base58.decode(input);
if ( store.length != 43 )
{
throw new Exception ("invalid key length for BIP38");
}
boolean ec = false;
boolean compressed = false;
boolean hasLot = false;
if ( (store[1] & 0xff) == 0x42 )
{
if ( (store[2] & 0xff) == 0xc0 )
{
// non-EC-multiplied keys without compression (prefix 6PR)
}
else if ( (store[2] & 0xff) == 0xe0 )
{
// non-EC-multiplied keys with compression (prefix 6PY)
compressed = true;
}
else
{
throw new Exception ("invalid key");
}
}
else if ( (store[1] & 0xff) == 0x43 )
{
// EC-multiplied keys without compression (prefix 6Pf)
// EC-multiplied keys with compression (prefix 6Pn)
ec = true;
compressed = (store[2] & 0x20) != 0;
hasLot = (store[2] & 0x04) != 0;
if ( (store[2] & 0x24) != store[2] )
{
throw new Exception ("invalid key");
}
}
else
{
throw new Exception ("invalid key");
}
byte[] checksum = new byte[4];
System.arraycopy (store, store.length - 4, checksum, 0, 4);
byte[] ekey = new byte[store.length - 4];
System.arraycopy (store, 0, ekey, 0, store.length - 4);
byte[] hash = hash (ekey);
for ( int i = 0; i < 4; ++i )
{
if ( hash[i] != checksum[i] )
{
throw new Exception ("checksum mismatch");
}
}
if ( ec == false )
{
return parseBIP38NoEC (store, password, compressed);
}
else
{
return parseBIP38EC (store, password, compressed, hasLot);
}
}
private static ECKey parseBIP38NoEC (byte[] store, String passphrase, boolean compressed) throws Exception
{
byte[] addressHash = new byte[4];
System.arraycopy (store, 3, addressHash, 0, 4);
byte[] derived = SCrypt.generate (passphrase.getBytes ("UTF-8"), addressHash, 16384, 8, 8, 64);
byte[] key = new byte[32];
System.arraycopy (derived, 32, key, 0, 32);
SecretKeySpec keyspec = new SecretKeySpec (key, "AES");
Cipher cipher = Cipher.getInstance ("AES/ECB/NoPadding", "BC");
cipher.init (Cipher.DECRYPT_MODE, keyspec);
byte[] decrypted = cipher.doFinal (store, 7, 32);
for ( int i = 0; i < 32; ++i )
{
decrypted[i] ^= derived[i];
}
byte[] appendZeroByte = ArrayUtils.addAll(new byte[1], decrypted);
ECKey kp = new ECKey (new BigInteger(appendZeroByte));
String address = null;
if (compressed) {
address = kp.toAddressCompressed(MainNetParams.get()).toString();
} else {
address = kp.toAddressUnCompressed(MainNetParams.get()).toString();
}
byte[] acs = hash (address.toString().getBytes ("US-ASCII"));
byte[] check = new byte[4];
System.arraycopy (acs, 0, check, 0, 4);
if ( !Arrays.equals (check, addressHash) )
{
throw new Exception ("failed to decrpyt");
}
return kp;
}
private static ECKey parseBIP38EC (byte[] store, String passphrase, boolean compressed, boolean hasLot) throws Exception
{
byte[] addressHash = new byte[4];
System.arraycopy (store, 3, addressHash, 0, 4);
byte[] ownentropy = new byte[8];
System.arraycopy (store, 7, ownentropy, 0, 8);
byte[] ownersalt = ownentropy;
if ( hasLot )
{
ownersalt = new byte[4];
System.arraycopy (ownentropy, 0, ownersalt, 0, 4);
}
byte[] passfactor = SCrypt.generate (passphrase.getBytes ("UTF-8"), ownersalt, 16384, 8, 8, 32);
if ( hasLot )
{
byte[] tmp = new byte[40];
System.arraycopy (passfactor, 0, tmp, 0, 32);
System.arraycopy (ownentropy, 0, tmp, 32, 8);
passfactor = hash (tmp);
}
byte[] appendZeroByte = ArrayUtils.addAll(new byte[1], passfactor);
ECKey kp = new ECKey (new BigInteger(appendZeroByte));
byte[] salt = new byte[12];
System.arraycopy (store, 3, salt, 0, 12);
byte[] derived = SCrypt.generate (kp.getPubKeyCompressed(), salt, 1024, 1, 1, 64);
byte[] aeskey = new byte[32];
System.arraycopy (derived, 32, aeskey, 0, 32);
SecretKeySpec keyspec = new SecretKeySpec (aeskey, "AES");
Cipher cipher = Cipher.getInstance ("AES/ECB/NoPadding", "BC");
cipher.init (Cipher.DECRYPT_MODE, keyspec);
byte[] encrypted = new byte[16];
System.arraycopy (store, 23, encrypted, 0, 16);
byte[] decrypted2 = cipher.doFinal (encrypted);
for ( int i = 0; i < 16; ++i )
{
decrypted2[i] ^= derived[i + 16];
}
System.arraycopy (store, 15, encrypted, 0, 8);
System.arraycopy (decrypted2, 0, encrypted, 8, 8);
byte[] decrypted1 = cipher.doFinal (encrypted);
for ( int i = 0; i < 16; ++i )
{
decrypted1[i] ^= derived[i];
}
byte[] seed = new byte[24];
System.arraycopy (decrypted1, 0, seed, 0, 16);
System.arraycopy (decrypted2, 8, seed, 16, 8);
BigInteger priv =
new BigInteger (1, passfactor).multiply (new BigInteger (1, hash (seed))).remainder (SECNamedCurves.getByName ("secp256k1").getN ());
kp = new ECKey (priv);
String address = null;
if (compressed) {
address = kp.toAddressCompressed(MainNetParams.get()).toString();
} else {
address = kp.toAddressUnCompressed(MainNetParams.get()).toString();
}
byte[] acs = hash (address.getBytes ("US-ASCII"));
byte[] check = new byte[4];
System.arraycopy (acs, 0, check, 0, 4);
if ( !Arrays.equals (check, addressHash) )
{
throw new Exception ("failed to decrpyt");
}
return kp;
}
}