package com.mygeopay.core.wallet;
import com.mygeopay.core.protos.Protos;
import org.bitcoinj.crypto.ChildNumber;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.crypto.EncryptedData;
import org.bitcoinj.crypto.KeyCrypter;
import org.bitcoinj.crypto.KeyCrypterException;
import org.bitcoinj.crypto.KeyCrypterScrypt;
import org.bitcoinj.store.UnreadableWalletException;
import org.bitcoinj.wallet.DeterministicSeed;
import com.google.common.base.Splitter;
import com.google.protobuf.ByteString;
import com.google.protobuf.TextFormat;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import static com.google.common.base.Preconditions.checkState;
/**
* @author John L. Jegutanis
*/
public class WalletProtobufSerializer {
/**
* Formats the given wallet (transactions and keys) to the given output stream in protocol buffer format.<p>
*
* Equivalent to <tt>walletToProto(wallet).writeTo(output);</tt>
*/
public static void writeWallet(Wallet wallet, OutputStream output) throws IOException {
Protos.Wallet walletProto = toProtobuf(wallet);
walletProto.writeTo(output);
}
/**
* Returns the given wallet formatted as text. The text format is that used by protocol buffers and although it
* can also be parsed using {@link TextFormat#merge(CharSequence, com.google.protobuf.Message.Builder)},
* it is designed more for debugging than storage. It is not well specified and wallets are largely binary data
* structures anyway, consisting as they do of keys (large random numbers) and
* {@link org.bitcoinj.core.Transaction}s which also mostly contain keys and hashes.
*/
public static String walletToText(Wallet wallet) {
Protos.Wallet walletProto = toProtobuf(wallet);
return TextFormat.printToString(walletProto);
}
/**
* Converts the given wallet to the object representation of the protocol buffers. This can be modified, or
* additional data fields set, before serialization takes place.
*/
public static Protos.Wallet toProtobuf(Wallet wallet) {
Protos.Wallet.Builder walletBuilder = Protos.Wallet.newBuilder();
// Populate the wallet version.
walletBuilder.setVersion(wallet.getVersion());
// Set the seed if exists
if (wallet.getSeed() != null) {
Protos.Key.Builder mnemonicEntry = SimpleKeyChain.serializeEncryptableItem(wallet.getSeed());
mnemonicEntry.setType(Protos.Key.Type.DETERMINISTIC_MNEMONIC);
walletBuilder.setSeed(mnemonicEntry.build());
}
// Set the master key
walletBuilder.setMasterKey(getMasterKeyProto(wallet));
// Populate the scrypt parameters.
KeyCrypter keyCrypter = wallet.getKeyCrypter();
if (keyCrypter == null) {
// The wallet is unencrypted.
walletBuilder.setEncryptionType(Protos.Wallet.EncryptionType.UNENCRYPTED);
} else {
// The wallet is encrypted.
if (keyCrypter instanceof KeyCrypterScrypt) {
KeyCrypterScrypt keyCrypterScrypt = (KeyCrypterScrypt) keyCrypter;
walletBuilder.setEncryptionType(Protos.Wallet.EncryptionType.ENCRYPTED_SCRYPT_AES);
// Bitcoinj format to our native protobuf
Protos.ScryptParameters.Builder encParamBuilder = Protos.ScryptParameters.newBuilder();
encParamBuilder.setSalt(keyCrypterScrypt.getScryptParameters().getSalt());
encParamBuilder.setR(keyCrypterScrypt.getScryptParameters().getR());
encParamBuilder.setP(keyCrypterScrypt.getScryptParameters().getP());
encParamBuilder.setN(keyCrypterScrypt.getScryptParameters().getN());
walletBuilder.setEncryptionParameters(encParamBuilder);
} else {
// Some other form of encryption has been specified that we do not know how to persist.
throw new RuntimeException("The wallet has encryption of type '" +
keyCrypter.getClass().toString() + "' but this WalletProtobufSerializer " +
"does not know how to persist this.");
}
}
// Add serialized pockets
for (WalletAccount account : wallet.getAllAccounts()) {
Protos.WalletPocket pocketProto;
if (account instanceof AbstractWallet) {
pocketProto = WalletPocketProtobufSerializer.toProtobuf((WalletPocketHD) account);
} else {
throw new RuntimeException("Implement serialization for: " + account.getClass());
}
walletBuilder.addPockets(pocketProto);
}
return walletBuilder.build();
}
private static Protos.Key getMasterKeyProto(Wallet wallet) {
DeterministicKey key = wallet.getMasterKey();
Protos.Key.Builder proto = SimpleKeyChain.serializeKey(key);
proto.setType(Protos.Key.Type.DETERMINISTIC_KEY);
final Protos.DeterministicKey.Builder detKey = proto.getDeterministicKeyBuilder();
detKey.setChainCode(ByteString.copyFrom(key.getChainCode()));
for (ChildNumber num : key.getPath()) {
detKey.addPath(num.i());
}
return proto.build();
}
/**
* <p>Parses a wallet from the given stream, using the provided Wallet instance to load data into. This is primarily
* used when you want to register extensions. Data in the proto will be added into the wallet where applicable and
* overwrite where not.</p>
*
* <p>A wallet can be unreadable for various reasons, such as inability to open the file, corrupt data, internally
* inconsistent data, a wallet extension marked as mandatory that cannot be handled and so on. You should always
* handle {@link UnreadableWalletException} and communicate failure to the user in an appropriate manner.</p>
*
* @throws UnreadableWalletException thrown in various error conditions (see description).
*/
public static Wallet readWallet(InputStream input) throws UnreadableWalletException {
try {
Protos.Wallet walletProto = parseToProto(input);
return readWallet(walletProto);
} catch (IOException e) {
throw new UnreadableWalletException("Could not parse input stream to protobuf", e);
}
}
/**
* <p>Loads wallet data from the given protocol buffer and inserts it into the given Wallet object. This is primarily
* useful when you wish to pre-register extension objects. Note that if loading fails the provided Wallet object
* may be in an indeterminate state and should be thrown away.</p>
*
* <p>A wallet can be unreadable for various reasons, such as inability to open the file, corrupt data, internally
* inconsistent data, a wallet extension marked as mandatory that cannot be handled and so on. You should always
* handle {@link UnreadableWalletException} and communicate failure to the user in an appropriate manner.</p>
*
* @throws UnreadableWalletException thrown in various error conditions (see description).
*/
public static Wallet readWallet(Protos.Wallet walletProto) throws UnreadableWalletException {
if (walletProto.getVersion() > 1)
throw new UnreadableWalletException.FutureVersion();
// Check if wallet is encrypted
final KeyCrypter crypter = getKeyCrypter(walletProto);
DeterministicSeed seed = null;
if (walletProto.hasSeed()) {
Protos.Key key = walletProto.getSeed();
if (key.hasSecretBytes()) {
List<String> mnemonic = Splitter.on(" ").splitToList(key.getSecretBytes().toStringUtf8());
seed = new DeterministicSeed(new byte[16], mnemonic, 0);
} else if (key.hasEncryptedData()) {
EncryptedData data = new EncryptedData(key.getEncryptedData().getInitialisationVector().toByteArray(),
key.getEncryptedData().getEncryptedPrivateKey().toByteArray());
seed = new DeterministicSeed(data, null, 0);
} else {
throw new UnreadableWalletException("Malformed key proto: " + key.toString());
}
}
DeterministicKey masterKey =
SimpleHDKeyChain.getDeterministicKey(walletProto.getMasterKey(), null, crypter);
Wallet wallet = new Wallet(masterKey, seed);
if (walletProto.hasVersion()) {
wallet.setVersion(walletProto.getVersion());
}
WalletPocketProtobufSerializer pocketSerializer = new WalletPocketProtobufSerializer();
for (Protos.WalletPocket pocketProto : walletProto.getPocketsList()) {
AbstractWallet pocket = pocketSerializer.readWallet(pocketProto, crypter);
wallet.addAccount(pocket);
}
return wallet;
}
private static KeyCrypter getKeyCrypter(Protos.Wallet walletProto) {
KeyCrypter crypter;
if (walletProto.hasEncryptionType()) {
if (walletProto.getEncryptionType() == Protos.Wallet.EncryptionType.ENCRYPTED_SCRYPT_AES) {
checkState(walletProto.hasEncryptionParameters(), "Encryption parameters are missing");
Protos.ScryptParameters encryptionParameters = walletProto.getEncryptionParameters();
org.bitcoinj.wallet.Protos.ScryptParameters.Builder bitcoinjCrypter =
org.bitcoinj.wallet.Protos.ScryptParameters.newBuilder();
bitcoinjCrypter.setSalt(encryptionParameters.getSalt());
bitcoinjCrypter.setN(encryptionParameters.getN());
bitcoinjCrypter.setP(encryptionParameters.getP());
bitcoinjCrypter.setR(encryptionParameters.getR());
crypter = new KeyCrypterScrypt(bitcoinjCrypter.build());
}
else if (walletProto.getEncryptionType() == Protos.Wallet.EncryptionType.UNENCRYPTED) {
crypter = null;
}
else {
throw new KeyCrypterException("Unsupported encryption: " + walletProto.getEncryptionType().toString());
}
}
else {
crypter = null;
}
return crypter;
}
/**
* Returns the loaded protocol buffer from the given byte stream. You normally want
* {@link Wallet#loadFromFile(java.io.File)} instead - this method is designed for low level work involving the
* wallet file format itself.
*/
public static Protos.Wallet parseToProto(InputStream input) throws IOException {
return Protos.Wallet.parseFrom(input);
}
}