/** * Copyright 2012 multibit.org * * Licensed under the MIT license (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://opensource.org/licenses/mit-license.php * * 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 org.multibit.file; import com.google.bitcoin.core.*; import com.google.bitcoin.crypto.KeyCrypter; import com.google.bitcoin.crypto.KeyCrypterException; import org.bitcoinj.wallet.Protos.Wallet.EncryptionType; import org.multibit.crypto.KeyCrypterOpenSSL; import org.multibit.utils.DateUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.spongycastle.crypto.params.KeyParameter; import java.io.*; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; /** * Class for handling reading and writing of private keys to a file. * * @author jim * */ public class PrivateKeysHandler { private Logger log = LoggerFactory.getLogger(PrivateKeysHandler.class); private static final String COMMENT_STRING_PREFIX = "#"; private static final int NUMBER_OF_MILLISECONDS_IN_A_SECOND = 1000; private SimpleDateFormat formatter; private NetworkParameters networkParameters; private static final String SEPARATOR = " "; private KeyCrypterOpenSSL keyCrypter; public PrivateKeysHandler(NetworkParameters networkParameters) { // Date format is UTC with century, T time separator and Z for UTC timezone. formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); formatter.setTimeZone(TimeZone.getTimeZone("UTC")); if (networkParameters == null) { throw new IllegalArgumentException("NetworkParameters must be supplied"); } this.networkParameters = networkParameters; keyCrypter = new KeyCrypterOpenSSL(); } public void exportPrivateKeys(File exportFile, Wallet wallet, BlockChain blockChain, boolean performEncryptionOfExportFile, CharSequence exportPassword, CharSequence walletPassword) throws IOException, KeyCrypterException { // Construct a StringBuffer with the private key export text. StringBuffer outputStringBuffer = new StringBuffer(); if (!performEncryptionOfExportFile) { outputHeaderComment(outputStringBuffer); } // Get the wallet's private keys and output them. Collection<PrivateKeyAndDate> keyAndDates = createKeyAndDates(wallet, blockChain, walletPassword); outputKeys(outputStringBuffer, keyAndDates); if (!performEncryptionOfExportFile) { outputFooterComment(outputStringBuffer); } String keyOutputText = outputStringBuffer.toString(); if (performEncryptionOfExportFile) { KeyCrypterOpenSSL keyCrypter = new KeyCrypterOpenSSL(); keyOutputText = keyCrypter.encrypt(keyOutputText, exportPassword); } FileWriter fileWriter = null; PrintWriter printWriter = null; try { fileWriter = new FileWriter(exportFile); printWriter = new PrintWriter(fileWriter); printWriter.write(keyOutputText); // Close the output stream. printWriter.close(); } finally { if (printWriter != null) { printWriter.close(); } if (fileWriter != null) { fileWriter.close(); } } } /** * Verify the export file was correctly written to disk. * * @param exportFile * The export file to verify * @param wallet * The wallet to verify the keys against * @param exportPassword * the password to use is encryption is required * @return Verification The result of verification * @throws KeyCrypterException */ public Verification verifyExportFile(File exportFile, Wallet wallet, BlockChain blockChain, CharSequence exportPassword, CharSequence walletPassword) throws KeyCrypterException { boolean thereWereFailures = false; String messageKey = "privateKeysHandler.failedForUnknownReason"; Object[] messageData = new Object[0]; try { // Create the expected export file contents. Collection<PrivateKeyAndDate> expectedKeysAndDates = createKeyAndDates(wallet, blockChain, walletPassword); // Read in the specified export file. Collection<PrivateKeyAndDate> importedKeysAndDates = readInPrivateKeys(exportFile, exportPassword); if (expectedKeysAndDates.size() != importedKeysAndDates.size()) { messageKey = "privateKeysHandler.wrongNumberOfKeys"; thereWereFailures = true; } else { for (int i = 0; i < expectedKeysAndDates.size(); i++) { Iterator<PrivateKeyAndDate> iteratorExpected = expectedKeysAndDates.iterator(); Iterator<PrivateKeyAndDate> iteratorImported = importedKeysAndDates.iterator(); PrivateKeyAndDate expected = iteratorExpected.next(); PrivateKeyAndDate imported = iteratorImported.next(); if (!Utils.bytesToHexString(expected.getKey().getPrivKeyBytes()).equals( Utils.bytesToHexString(imported.getKey().getPrivKeyBytes()))) { messageKey = "privateKeysHandler.keysDidNotMatch"; thereWereFailures = true; break; } // Imported keydate must be at or before expected (further back in time is safe). if ((imported.getDate() != null && imported.getDate().after(expected.getDate())) || (imported.getDate() == null && expected.getDate() != null)) { messageKey = "privateKeysHandler.keysDidNotMatch"; thereWereFailures = true; break; } } } } catch (PrivateKeysHandlerException pkhe) { messageKey = "privateKeysHandler.thereWasAnException"; messageData = new Object[] { pkhe.getMessage() }; thereWereFailures = true; } if (!thereWereFailures) { messageKey = "privateKeysHandler.verificationSuccess"; } return new Verification(!thereWereFailures, messageKey, messageData); } public Collection<PrivateKeyAndDate> readInPrivateKeys(File importFile, CharSequence password) throws PrivateKeysHandlerException, KeyCrypterException { if (importFile == null) { throw new PrivateKeysHandlerException("Import file cannot be null"); } ArrayList<PrivateKeyAndDate> parseResults = new ArrayList<PrivateKeyAndDate>(); Scanner scanner = null; try { // Read in the file. String importFileContents = readFile(importFile); if (importFileContents != null && importFileContents.startsWith(keyCrypter.getOpenSSLMagicText())) { // Decryption required. KeyCrypterOpenSSL keyCrypter = new KeyCrypterOpenSSL(); importFileContents = keyCrypter.decrypt(importFileContents, password); } scanner = new Scanner(new StringReader(importFileContents)); // First use a Scanner to get each line. while (scanner.hasNextLine()) { processLine(scanner.nextLine(), parseResults); } } catch (IOException ioe) { throw new PrivateKeysHandlerException("Could not read import file '" + importFile.getAbsolutePath() + "'", ioe); } finally { // Ensure the underlying stream is always closed // this only has any effect if the item passed // to the Scanner // constructor implements Closeable (which it // does in this case). if (scanner != null) { scanner.close(); } } return parseResults; } private void outputHeaderComment(StringBuffer out) { out.append("# KEEP YOUR PRIVATE KEYS SAFE !").append("\n"); out.append("# Anyone who can read this file can spend your bitcoin.").append("\n"); out.append("#").append("\n"); out.append("# Format:").append("\n"); out.append("# <Base58 encoded private key>[<whitespace>[<key createdAt>]]").append("\n"); out.append("#").append("\n"); out.append("# The Base58 encoded private keys are the same format as").append("\n"); out.append("# produced by the Satoshi client/ sipa dumpprivkey utility.").append("\n"); out.append("#").append("\n"); out.append("# Key createdAt is in UTC format as specified by ISO 8601").append("\n"); out.append("# e.g: 2011-12-31T16:42:00Z . The century, 'T' and 'Z' are mandatory").append("\n"); out.append("#").append("\n"); } private Collection<PrivateKeyAndDate> createKeyAndDates(Wallet wallet, BlockChain blockChain, CharSequence walletPassword) throws KeyCrypterException { // Determine if keys need to be decrypted. boolean decryptionRequired = false; Collection<ECKey> keychain = wallet.getKeychain(); Collection<PrivateKeyAndDate> keyAndDates = new ArrayList<PrivateKeyAndDate>(); synchronized (keychain) { if (wallet != null) { // Wallet keys need to be decrypted before output. if (wallet.getEncryptionType() != EncryptionType.UNENCRYPTED) { decryptionRequired = true; } } Set<Transaction> allTransactions = wallet.getTransactions(true); if (keychain != null) { HashMap<ECKey, Date> keyToEarliestUsageDateMap = new HashMap<ECKey, Date>(); // The date of the last transaction in the wallet - used where // there are no tx for a key. Date overallLastUsageDate = null; for (ECKey ecKey : keychain) { // Find the earliest usage of this key. Date earliestUsageDate = null; if (allTransactions != null) { for (Transaction tx : allTransactions) { if (transactionUsesKey(tx, ecKey)) { Date updateTime = tx.getUpdateTime(); if (updateTime != null) { if (overallLastUsageDate == null) { overallLastUsageDate = updateTime; } else { overallLastUsageDate = overallLastUsageDate.after(updateTime) ? overallLastUsageDate : updateTime; } if (earliestUsageDate == null) { earliestUsageDate = updateTime; } else { earliestUsageDate = earliestUsageDate.before(updateTime) ? earliestUsageDate : updateTime; } } } } } if (earliestUsageDate != null) { keyToEarliestUsageDateMap.put(ecKey, earliestUsageDate); } } // If there are no transactions in the wallet // overallLastUsageDate will be null. // We do not want keys output with a missing date as this forces // a replay from the genesis block // In this case we know there are no transactions up to the date // of the head of the // chain so can set the overallLastUsageDate to then. // On import this will replay from the current chain head to // include any future tx. if (overallLastUsageDate == null) { if (blockChain != null) { StoredBlock chainHead = blockChain.getChainHead(); if (chainHead != null) { Block header = chainHead.getHeader(); if (header != null) { long timeSeconds = header.getTimeSeconds(); if (timeSeconds != 0) { overallLastUsageDate = new Date(timeSeconds * NUMBER_OF_MILLISECONDS_IN_A_SECOND); } } } } } KeyCrypter walletKeyCrypter = wallet.getKeyCrypter(); KeyParameter aesKey = null; if (decryptionRequired) { aesKey = walletKeyCrypter.deriveKey(walletPassword); } for (ECKey ecKey : keychain) { Date earliestUsageDate = keyToEarliestUsageDateMap.get(ecKey); if (earliestUsageDate == null) { if (overallLastUsageDate != null) { // Put the last tx date for the whole wallet in for // this key - there are no tx for this key so this // will be early enough. earliestUsageDate = overallLastUsageDate; } } if (decryptionRequired) { // Create a new decrypted key holding the private key. ECKey decryptedKey = ecKey.decrypt(walletKeyCrypter, aesKey); keyAndDates.add(new PrivateKeyAndDate(decryptedKey, earliestUsageDate)); } else { keyAndDates.add(new PrivateKeyAndDate(ecKey, earliestUsageDate)); } } } } return keyAndDates; } private void outputKeys(StringBuffer out, Collection<PrivateKeyAndDate> keyAndDates) { for (PrivateKeyAndDate privateKeyAndDate : keyAndDates) { DumpedPrivateKey dumpedPrivateKey = privateKeyAndDate.getKey().getPrivateKeyEncoded(networkParameters); String keyOutputString = dumpedPrivateKey.toString(); if (privateKeyAndDate.getDate() != null) { keyOutputString = keyOutputString + SEPARATOR + formatter.format(privateKeyAndDate.getDate()); } out.append(keyOutputString).append("\n"); } } private void outputFooterComment(StringBuffer out) { out.append("# End of private keys").append("\n"); } public Date calculateReplayDate(Collection<PrivateKeyAndDate> privateKeyAndDates, Wallet wallet) { boolean thereWereMissingDates = false; Date replayDate = new Date(DateUtils.nowUtc().getMillis()); for (PrivateKeyAndDate loop : privateKeyAndDates) { if (loop.getDate() == null) { thereWereMissingDates = true; } else { if (loop.getKey() != null) { if (wallet != null && !keyChainContainsPrivateKey(wallet.getKeychain(), loop.getKey())) { replayDate = replayDate.before(loop.getDate()) ? replayDate : loop.getDate(); } } } } if (thereWereMissingDates) { return null; } else { return replayDate; } } /** * This method is here because there is no equals on ECKey. */ private boolean keyChainContainsPrivateKey(Collection<ECKey> keyChain, ECKey keyToAdd) { if (keyChain == null || keyToAdd == null) { return false; } else { for (ECKey loopKey : keyChain) { if (Arrays.equals(keyToAdd.getPrivKeyBytes(), loopKey.getPrivKeyBytes())) { return true; } } return false; } } private boolean transactionUsesKey(Transaction transaction, ECKey ecKey) { for (TransactionOutput output : transaction.getOutputs()) { // This is not thread safe as a key could be removed between the // call to isMine and receive. try { byte[] pubkeyHash = output.getScriptPubKey().getPubKeyHash(); if (Arrays.equals(ecKey.getPubKeyHash(), pubkeyHash)) { return true; } } catch (ScriptException e) { log.error("Could not parse tx output script: {}", e.toString()); return false; } } for (TransactionInput input : transaction.getInputs()) { // This is not thread safe as a key could be removed between the // call to isPubKeyMine and receive. try { byte[] pubkey = input.getScriptSig().getPubKey(); if (Arrays.equals(ecKey.getPubKey(), pubkey)) { return true; } } catch (ScriptException e) { log.error("Could not parse tx output script: {}", e.toString()); return false; } } return false; } private void processLine(String line, ArrayList<PrivateKeyAndDate> parseResults) { if (line != null && !line.trim().equals("") && !line.startsWith(COMMENT_STRING_PREFIX)) { Scanner scanner = null; try { scanner = new Scanner(line); String sipaKey = ""; String createdAtAsString = ""; if (scanner.hasNext()) { sipaKey = scanner.next(); } if (scanner.hasNext()) { createdAtAsString = scanner.next(); } DumpedPrivateKey dumpedPrivateKey = new DumpedPrivateKey(networkParameters, sipaKey); PrivateKeyAndDate privateKeyAndDate = new PrivateKeyAndDate(); privateKeyAndDate.setKey(dumpedPrivateKey.getKey()); if (createdAtAsString != null && !"".equals(createdAtAsString)) { Date date = formatter.parse(createdAtAsString); privateKeyAndDate.setDate(date); } parseResults.add(privateKeyAndDate); } catch (AddressFormatException e) { throw new PrivateKeysHandlerException("Could not understand address in import file", e); } catch (ParseException pe) { throw new PrivateKeysHandlerException("Could not parse date in import file", pe); } finally { if (scanner != null) { scanner.close(); } } } } public static String readFile(File file) throws IOException { BufferedReader reader = new BufferedReader(new FileReader(file)); String line; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(""); String ls = System.getProperty("line.separator"); while ((line = reader.readLine()) != null) { stringBuilder.append(line); stringBuilder.append(ls); } return stringBuilder.toString(); } }