// Copyright (C) 2013-2014 Bonsai Software, Inc. // // 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.bonsai.wallet32; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.security.SecureRandom; import java.util.Hashtable; import java.util.List; import org.bitcoinj.wallet.Protos; import org.bitcoinj.wallet.Protos.ScryptParameters; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.spongycastle.crypto.params.KeyParameter; import org.spongycastle.util.encoders.Hex; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Color; import com.google.bitcoin.core.AddressFormatException; import com.google.bitcoin.core.Base58; import com.google.bitcoin.core.ECKey; import com.google.bitcoin.core.NetworkParameters; import com.google.bitcoin.core.ScriptException; import com.google.bitcoin.core.Transaction; import com.google.bitcoin.core.Transaction.SigHash; import com.google.bitcoin.core.TransactionInput; import com.google.bitcoin.core.TransactionOutput; import com.google.bitcoin.core.Utils; import com.google.bitcoin.crypto.DeterministicKey; import com.google.bitcoin.crypto.HDKeyDerivation; import com.google.bitcoin.crypto.KeyCrypter; import com.google.bitcoin.crypto.KeyCrypterScrypt; import com.google.bitcoin.crypto.MnemonicCodeX; import com.google.bitcoin.crypto.TransactionSignature; import com.google.bitcoin.script.Script; import com.google.bitcoin.script.ScriptBuilder; import com.google.protobuf.ByteString; 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; public class WalletUtil { private static Logger mLogger = LoggerFactory.getLogger(WalletUtil.class); private static final String filePrefix = "wallet32"; private final static QRCodeWriter sQRCodeWriter = new QRCodeWriter(); @SuppressLint("TrulyRandom") public static void setPasscode(Context context, WalletService walletService, String passcode, boolean isChange) { WalletApplication wallapp = (WalletApplication) context.getApplicationContext(); KeyParameter oldAesKey = wallapp.mAesKey; mLogger.info("setPasscode starting"); byte[] salt; if (isChange) { // Reuse our salt (better chance of recovering if // we are between passcode values). salt = readSalt(context); } else { // Create salt and write to file. SecureRandom secureRandom = new SecureRandom(); salt = new byte[KeyCrypterScrypt.SALT_LENGTH]; secureRandom.nextBytes(salt); writeSalt(context, salt); } KeyCrypter keyCrypter = getKeyCrypter(salt); KeyParameter aesKey = keyCrypter.deriveKey(passcode); if (isChange) { walletService.changePasscode(oldAesKey, keyCrypter, aesKey); } // Set up the application context with credentials. wallapp.mPasscode = passcode; wallapp.mKeyCrypter = keyCrypter; wallapp.mAesKey = aesKey; mLogger.info("setPasscode finished"); } public static void createWallet(Context context) { WalletApplication wallapp = (WalletApplication) context.getApplicationContext(); NetworkParameters params = Constants.getNetworkParameters(context); // Generate a new seed. SecureRandom random = new SecureRandom(); byte seed[] = new byte[16]; random.nextBytes(seed); // We currently don't use BIP-0039 passphrases. String passphrase = ""; int numAccounts = 2; // New wallets are version V0_6. MnemonicCodeX.Version bip39version = MnemonicCodeX.Version.V0_6; // Setup a wallet with the seed. HDWallet hdwallet = new HDWallet(wallapp, params, wallapp.mKeyCrypter, wallapp.mAesKey, seed, passphrase, numAccounts, bip39version, HDWallet.HDStructVersion.HDSV_STDV1); hdwallet.persist(wallapp); } public static boolean passcodeValid(Context context, String passcode) { WalletApplication wallapp = (WalletApplication) context.getApplicationContext(); byte[] salt = readSalt(context); KeyCrypter keyCrypter = getKeyCrypter(salt); KeyParameter aesKey = keyCrypter.deriveKey(passcode); // Can we parse our wallet file? try { HDWallet.deserialize(wallapp, keyCrypter, aesKey); } catch (Exception ex) { mLogger.warn("passcode didn't deserialize wallet"); return false; } // It worked so we consider it valid ... // Set up the application context with credentials. wallapp.mPasscode = passcode; wallapp.mKeyCrypter = keyCrypter; wallapp.mAesKey = aesKey; return true; } public static void writeSalt(Context context, byte[] salt) { WalletApplication wallapp = (WalletApplication) context.getApplicationContext(); mLogger.info("writing salt " + new String(Hex.encode(salt))); File saltFile = new File(wallapp.getWalletDir(), "salt"); FileOutputStream saltStream; try { saltStream = new FileOutputStream(saltFile); saltStream.write(salt); saltStream.close(); } catch (FileNotFoundException ex) { ex.printStackTrace(); } catch (IOException ex) { ex.printStackTrace(); } } public static byte[] readSalt(Context context) { WalletApplication wallapp = (WalletApplication) context.getApplicationContext(); File saltFile = new File(wallapp.getWalletDir(), "salt"); byte[] salt = new byte[(int) saltFile.length()]; DataInputStream dis; try { dis = new DataInputStream(new FileInputStream(saltFile)); dis.readFully(salt); dis.close(); // mLogger.info("read salt " + new String(Hex.encode(salt))); return salt; } catch (FileNotFoundException ex) { ex.printStackTrace(); } catch (IOException ex) { ex.printStackTrace(); } return null; } public static KeyCrypter getKeyCrypter(byte[] salt) { Protos.ScryptParameters.Builder scryptParametersBuilder = Protos.ScryptParameters.newBuilder() .setSalt(ByteString.copyFrom(salt)); ScryptParameters scryptParameters = scryptParametersBuilder.build(); return new KeyCrypterScrypt(scryptParameters); } public static byte[] msgHexToBytes(String hexstr) { byte[] msgbytes = Hex.decode(hexstr); return Utils.reverseBytes(msgbytes); } public static Bitmap createBitmap(String content, final int size) { final Hashtable<EncodeHintType, Object> hints = new Hashtable<EncodeHintType, Object>(); hints.put(EncodeHintType.MARGIN, 0); hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); BitMatrix result; try { result = sQRCodeWriter.encode(content, BarcodeFormat.QR_CODE, size, size, hints); } catch (WriterException ex) { mLogger.warn("qr encoder failed: " + ex.toString()); return null; } 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; } // Thanks to devrandom! public static void signTransactionInputs(Transaction tx, SigHash hashType, ECKey key, List<Script> inputScripts) throws ScriptException { List<TransactionInput> inputs = tx.getInputs(); List<TransactionOutput> outputs = tx.getOutputs(); checkState(inputs.size() > 0); checkState(outputs.size() > 0); checkArgument(hashType == SigHash.ALL, "Only SIGHASH_ALL is currently supported"); // The transaction is signed with the input scripts empty // except for the input we are signing. In the case where // addInput has been used to set up a new transaction, they // are already all empty. The input being signed has to have // the connected OUTPUT program in it when the hash is // calculated! // // Note that each input may be claiming an output sent to a // different key. So we have to look at the outputs to figure // out which key to sign with. TransactionSignature[] signatures = new TransactionSignature[inputs.size()]; ECKey[] signingKeys = new ECKey[inputs.size()]; for (int i = 0; i < inputs.size(); i++) { TransactionInput input = inputs.get(i); // We don't have the connected output, we assume it was // signed already and move on if (input.getScriptBytes().length != 0) mLogger.warn("Re-signing an already signed transaction! Be sure this is what you want."); // This assert should never fire. If it does, it means the wallet is inconsistent. checkNotNull(key, "Transaction exists in wallet that we cannot redeem: %s", input.getOutpoint().getHash()); // Keep the key around for the script creation step below. signingKeys[i] = key; // The anyoneCanPay feature isn't used at the moment. boolean anyoneCanPay = false; signatures[i] = tx.calculateSignature(i, key, inputScripts.get(i), hashType, anyoneCanPay); } // Now we have calculated each signature, go through and // create the scripts. Reminder: the script consists: // // 1) For pay-to-address outputs: a signature (over a hash of // the simplified transaction) and the complete public key // needed to sign for the connected output. The output script // checks the provided pubkey hashes to the address and then // checks the signature. // // 2) For pay-to-key outputs: just a signature. // for (int i = 0; i < inputs.size(); i++) { if (signatures[i] == null) continue; TransactionInput input = inputs.get(i); Script scriptPubKey = inputScripts.get(i); if (scriptPubKey.isSentToAddress()) { input.setScriptSig(ScriptBuilder.createInputScript(signatures[i], signingKeys[i])); } else if (scriptPubKey.isSentToRawPubKey()) { input.setScriptSig(ScriptBuilder.createInputScript(signatures[i])); } else { // Should be unreachable - if we don't recognize // the type of script we're trying to sign for, // then we should have failed above when fetching // the key to sign with. throw new RuntimeException("Do not understand script type: " + scriptPubKey); } } // Every input is now complete. } public static DeterministicKey createMasterPubKeyFromPubB58(String xpubstr) throws AddressFormatException { byte[] data = Base58.decodeChecked(xpubstr); ByteBuffer ser = ByteBuffer.wrap(data); if (ser.getInt() != 0x0488B21E) throw new AddressFormatException("bad xpub version"); ser.get(); // depth ser.getInt(); // parent fingerprint ser.getInt(); // child number byte[] chainCode = new byte[32]; ser.get(chainCode); byte[] pubBytes = new byte[33]; ser.get(pubBytes); return HDKeyDerivation.createMasterPubKeyFromBytes(pubBytes, chainCode); } } // Local Variables: // mode: java // c-basic-offset: 4 // tab-width: 4 // End: