Fix BIP39 implementation

This commit is contained in:
Devrandom
2014-07-07 17:11:41 -07:00
committed by Mike Hearn
parent 3420bdf8ac
commit 2fae12064c
10 changed files with 320 additions and 135 deletions

View File

@@ -22,7 +22,8 @@ import com.google.bitcoin.core.Utils;
import com.google.bitcoin.crypto.*;
import com.google.bitcoin.store.UnreadableWalletException;
import com.google.bitcoin.utils.Threading;
import com.google.common.base.Preconditions;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.protobuf.ByteString;
import org.bitcoinj.wallet.Protos;
@@ -32,6 +33,8 @@ import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.math.ec.ECPoint;
import javax.annotation.Nullable;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.ArrayList;
@@ -77,10 +80,6 @@ import static com.google.common.collect.Lists.newLinkedList;
*/
public class DeterministicKeyChain implements EncryptableKeyChain {
private static final Logger log = LoggerFactory.getLogger(DeterministicKeyChain.class);
// It would take more than 10^12 years to brute-force a 128 bit seed using $1B worth
// of computing equipment.
public static final int DEFAULT_SEED_BITS = 128;
public static final int MAX_SEED_BITS = 512;
private final ReentrantLock lock = Threading.lock("DeterministicKeyChain");
@@ -126,29 +125,28 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
private boolean isFollowing;
/**
* Generates a new key chain with a seed selected randomly from the given {@link java.security.SecureRandom}
* object and the default seed size.
* Generates a new key chain with entropy selected randomly from the given {@link java.security.SecureRandom}
* object and the default entropy size.
*/
public DeterministicKeyChain(SecureRandom random) {
this(getRandomSeed(random, DEFAULT_SEED_BITS), Utils.currentTimeSeconds());
this(random, DeterministicSeed.DEFAULT_SEED_ENTROPY_BITS, "", Utils.currentTimeSeconds());
}
/**
* Generates a new key chain with a seed selected randomly from the given {@link java.security.SecureRandom}
* Generates a new key chain with entropy selected randomly from the given {@link java.security.SecureRandom}
* object and of the requested size in bits.
*/
public DeterministicKeyChain(SecureRandom random, int bits) {
this(getRandomSeed(random, bits), Utils.currentTimeSeconds());
this(random, bits, "", Utils.currentTimeSeconds());
}
private static byte[] getRandomSeed(SecureRandom random, int bits) {
Preconditions.checkArgument(bits >= DEFAULT_SEED_BITS, "requested seed size too small");
Preconditions.checkArgument(bits <= MAX_SEED_BITS, "requested seed size too large");
Preconditions.checkArgument(bits % 8 == 0, "requested seed size not an even number of bytes");
byte[] seed = new byte[bits / 8];
random.nextBytes(seed);
return seed;
/**
* Generates a new key chain with entropy selected randomly from the given {@link java.security.SecureRandom}
* object and of the requested size in bits. The derived seed is further protected with a user selected passphrase
* (see BIP 39).
*/
public DeterministicKeyChain(SecureRandom random, int bits, String passphrase, long seedCreationTimeSecs) {
this(new DeterministicSeed(random, bits, passphrase, seedCreationTimeSecs));
}
/**
@@ -496,10 +494,10 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
}
/** Returns a list of words that represent the seed. */
public List<String> toMnemonicCode() {
public List<String> getMnemonicCode() {
lock.lock();
try {
return seed.toMnemonicCode();
return seed.getMnemonicCode();
} finally {
lock.unlock();
}
@@ -529,6 +527,11 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
Protos.Key.Builder seedEntry = BasicKeyChain.serializeEncryptableItem(seed);
seedEntry.setType(Protos.Key.Type.DETERMINISTIC_ROOT_SEED);
entries.add(seedEntry.build());
if (seed.hasMnemonicCode()) {
Protos.Key.Builder mnemonicEntry = BasicKeyChain.serializeEncryptableItem(seed.getMnemonicEncryptableItem());
mnemonicEntry.setType(Protos.Key.Type.DETERMINISTIC_MNEMONIC);
entries.add(mnemonicEntry.build());
}
}
Map<ECKey, Protos.Key.Builder> keys = basicKeyChain.serializeToEditableProtobufs();
for (Map.Entry<ECKey, Protos.Key.Builder> entry : keys.entrySet()) {
@@ -573,7 +576,19 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
int lookaheadSize = -1;
for (Protos.Key key : keys) {
final Protos.Key.Type t = key.getType();
if (t == Protos.Key.Type.DETERMINISTIC_ROOT_SEED) {
if (t == Protos.Key.Type.DETERMINISTIC_MNEMONIC) {
checkState(chain == null);
checkState(seed != null);
if (key.hasSecretBytes()) {
seed.setMnemonicCode(key.getSecretBytes().toByteArray());
} else if (key.hasEncryptedData()) {
EncryptedData data = new EncryptedData(key.getEncryptedData().getInitialisationVector().toByteArray(),
key.getEncryptedData().getEncryptedPrivateKey().toByteArray());
seed.setEncryptedMnemonicCode(data);
} else {
throw new UnreadableWalletException("Malformed key proto: " + key.toString());
}
} else if (t == Protos.Key.Type.DETERMINISTIC_ROOT_SEED) {
if (chain != null) {
checkState(lookaheadSize >= 0);
chain.setLookaheadSize(lookaheadSize);

View File

@@ -18,11 +18,18 @@
package com.google.bitcoin.wallet;
import com.google.bitcoin.crypto.*;
import com.google.bitcoin.store.UnreadableWalletException;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import org.bitcoinj.wallet.Protos;
import org.spongycastle.crypto.params.KeyParameter;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.List;
@@ -36,44 +43,99 @@ import static com.google.common.base.Preconditions.checkState;
* code.
*/
public class DeterministicSeed implements EncryptableItem {
// It would take more than 10^12 years to brute-force a 128 bit seed using $1B worth
// of computing equipment.
public static final int DEFAULT_SEED_ENTROPY_BITS = 128;
public static final int MAX_SEED_ENTROPY_BITS = 512;
public static final String UTF_8 = "UTF-8";
@Nullable private final byte[] unencryptedSeed;
@Nullable private final EncryptedData encryptedSeed;
@Nullable private List<String> mnemonicCode;
@Nullable private EncryptedData encryptedMnemonicCode;
private final long creationTimeSeconds;
private static MnemonicCode MNEMONIC_CODE;
private static synchronized MnemonicCode getCachedMnemonicCode() {
private static MnemonicCode MNEMONIC_CODEC;
private static synchronized MnemonicCode getCachedMnemonicCodec() {
try {
// This object can be large and has to load the word list from disk, so we lazy cache it.
if (MNEMONIC_CODE == null) {
MNEMONIC_CODE = new MnemonicCode();
if (MNEMONIC_CODEC == null) {
MNEMONIC_CODEC = new MnemonicCode();
}
return MNEMONIC_CODE;
return MNEMONIC_CODEC;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public DeterministicSeed(byte[] unencryptedSeed, long creationTimeSeconds) {
DeterministicSeed(byte[] unencryptedSeed, List<String> mnemonic, long creationTimeSeconds) {
this.unencryptedSeed = checkNotNull(unencryptedSeed);
this.encryptedSeed = null;
this.mnemonicCode = mnemonic;
this.encryptedMnemonicCode = null;
this.creationTimeSeconds = creationTimeSeconds;
}
public DeterministicSeed(EncryptedData encryptedSeed, long creationTimeSeconds) {
/**
* Constructs a seed from bytes. The mnemonic phrase is unknown.
*/
public DeterministicSeed(byte[] unencryptedSeed, long creationTimeSeconds) {
this(unencryptedSeed, null, creationTimeSeconds);
}
DeterministicSeed(EncryptedData encryptedSeed, EncryptedData encryptedMnemonic, long creationTimeSeconds) {
this.unencryptedSeed = null;
this.mnemonicCode = null;
this.encryptedSeed = checkNotNull(encryptedSeed);
this.encryptedMnemonicCode = encryptedMnemonic;
this.creationTimeSeconds = creationTimeSeconds;
}
DeterministicSeed(EncryptedData encryptedSeed, long creationTimeSeconds) {
this(encryptedSeed, null, creationTimeSeconds);
}
/**
* Constructs a seed from a BIP 39 mnemonic code. See {@link com.google.bitcoin.crypto.MnemonicCode} for more
* details on this scheme.
* @param words A list of words.
* @param mnemonicCode A list of words.
* @param passphrase A user supplied passphrase, or an empty string if there is no passphrase
* @param creationTimeSeconds When the seed was originally created, UNIX time.
* @throws MnemonicException if there is a problem decoding the words.
*/
public DeterministicSeed(List<String> words, long creationTimeSeconds) throws MnemonicException.MnemonicChecksumException, MnemonicException.MnemonicLengthException, MnemonicException.MnemonicWordException {
this(getCachedMnemonicCode().toEntropy(words), creationTimeSeconds);
public DeterministicSeed(List<String> mnemonicCode, String passphrase, long creationTimeSeconds) {
this(getCachedMnemonicCodec().toSeed(mnemonicCode, passphrase), mnemonicCode, creationTimeSeconds);
}
/**
* Constructs a seed from a BIP 39 mnemonic code. See {@link com.google.bitcoin.crypto.MnemonicCode} for more
* details on this scheme.
* @param random Entropy source
* @param bits number of bits, must be divisible by 32
* @param passphrase A user supplied passphrase, or an empty string if there is no passphrase
* @param creationTimeSeconds When the seed was originally created, UNIX time.
*/
public DeterministicSeed(SecureRandom random, int bits, String passphrase, long creationTimeSeconds) {
byte[] entropy = getEntropy(random, bits);
try {
this.mnemonicCode = getCachedMnemonicCodec().toMnemonic(entropy);
} catch (MnemonicException.MnemonicLengthException e) {
// cannot happen
throw new RuntimeException(e);
}
this.unencryptedSeed = getCachedMnemonicCodec().toSeed(mnemonicCode, passphrase);
this.encryptedSeed = null;
this.creationTimeSeconds = creationTimeSeconds;
}
private static byte[] getEntropy(SecureRandom random, int bits) {
Preconditions.checkArgument(bits >= DEFAULT_SEED_ENTROPY_BITS, "requested entropy size too small");
Preconditions.checkArgument(bits <= MAX_SEED_ENTROPY_BITS, "requested entropy size too large");
Preconditions.checkArgument(bits % 32 == 0, "requested entropy size not divisible by 32");
byte[] seed = new byte[bits / 8];
random.nextBytes(seed);
return seed;
}
@Override
@@ -87,7 +149,8 @@ public class DeterministicSeed implements EncryptableItem {
if (isEncrypted())
return "DeterministicSeed [encrypted]";
else
return "DeterministicSeed " + toHexString();
return "DeterministicSeed " + toHexString() +
((mnemonicCode != null) ? " " + Joiner.on(" ").join(mnemonicCode) : "");
}
/** Returns the seed as hex or null if encrypted. */
@@ -121,34 +184,66 @@ public class DeterministicSeed implements EncryptableItem {
return creationTimeSeconds;
}
public EncryptableItem getMnemonicEncryptableItem() {
return new EncryptableItem() {
@Override
public boolean isEncrypted() {
return DeterministicSeed.this.isEncrypted();
}
@Nullable
@Override
public byte[] getSecretBytes() {
return getMnemonicAsBytes();
}
@Nullable
@Override
public EncryptedData getEncryptedData() {
return encryptedMnemonicCode;
}
@Override
public Protos.Wallet.EncryptionType getEncryptionType() {
return Protos.Wallet.EncryptionType.ENCRYPTED_SCRYPT_AES;
}
@Override
public long getCreationTimeSeconds() {
return creationTimeSeconds;
}
};
}
public DeterministicSeed encrypt(KeyCrypter keyCrypter, KeyParameter aesKey) {
checkState(encryptedSeed == null, "Trying to encrypt seed twice");
checkState(unencryptedSeed != null, "Seed bytes missing so cannot encrypt");
EncryptedData data = keyCrypter.encrypt(unencryptedSeed, aesKey);
return new DeterministicSeed(data, creationTimeSeconds);
EncryptedData seed = keyCrypter.encrypt(unencryptedSeed, aesKey);
EncryptedData mnemonic = (mnemonicCode != null) ? keyCrypter.encrypt(getMnemonicAsBytes(), aesKey) : null;
return new DeterministicSeed(seed, mnemonic, creationTimeSeconds);
}
private byte[] getMnemonicAsBytes() {
try {
return Joiner.on(" ").join(mnemonicCode).getBytes(UTF_8);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
public DeterministicSeed decrypt(KeyCrypter crypter, KeyParameter aesKey) {
checkState(isEncrypted());
checkNotNull(encryptedSeed);
return new DeterministicSeed(crypter.decrypt(encryptedSeed, aesKey), creationTimeSeconds);
}
/** Returns a list of words that represent the seed, or IllegalStateException if the seed is encrypted or missing. */
public List<String> toMnemonicCode(MnemonicCode code) {
byte[] seed = crypter.decrypt(encryptedSeed, aesKey);
List<String> mnemonic = null;
try {
if (isEncrypted())
throw new IllegalStateException("The seed is encrypted");
final byte[] seed = checkNotNull(getSecretBytes());
return code.toMnemonic(seed);
} catch (MnemonicException.MnemonicLengthException e) {
throw new RuntimeException(e); // Cannot happen.
if (encryptedMnemonicCode != null)
mnemonic = decodeMnemonicCode(crypter.decrypt(encryptedMnemonicCode, aesKey));
} catch (UnreadableWalletException e) {
// TODO what is the best way to handle this exception?
throw new RuntimeException(e);
}
}
/** Returns a list of words that represent the seed, or IllegalStateException if the seed is encrypted or missing. */
public List<String> toMnemonicCode() {
return toMnemonicCode(getCachedMnemonicCode());
return new DeterministicSeed(seed, mnemonic, creationTimeSeconds);
}
@Override
@@ -175,4 +270,46 @@ public class DeterministicSeed implements EncryptableItem {
result = 31 * result + (int) (creationTimeSeconds ^ (creationTimeSeconds >>> 32));
return result;
}
/**
* Check if our mnemonic is a valid mnemonic phrase for our word list.
* Does nothing if we are encrypted.
*
* @throws com.google.bitcoin.crypto.MnemonicException if check fails
*/
public void check() throws MnemonicException {
if (mnemonicCode != null)
getCachedMnemonicCodec().check(mnemonicCode);
}
/** Get the mnemonic code, or null if unknown. */
@Nullable
public List<String> getMnemonicCode() {
return mnemonicCode;
}
/** Set encrypted mnemonic code. Used by protobuf deserializer. */
public void setEncryptedMnemonicCode(EncryptedData encryptedMnemonicCode) {
this.encryptedMnemonicCode = encryptedMnemonicCode;
}
/** Set mnemonic code from UTF-8 encoded bytes. */
public void setMnemonicCode(@Nullable byte[] mnemonicCode) throws UnreadableWalletException {
this.mnemonicCode = decodeMnemonicCode(mnemonicCode);
}
/** Whether the mnemonic code is known for this seed. */
public boolean hasMnemonicCode() {
return mnemonicCode != null || encryptedMnemonicCode != null;
}
private List<String> decodeMnemonicCode(byte[] mnemonicCode) throws UnreadableWalletException {
String code = null;
try {
code = new String(mnemonicCode, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new UnreadableWalletException(e.toString());
}
return Splitter.on(" ").splitToList(code);
}
}

View File

@@ -188,7 +188,7 @@ public class KeyChainGroup {
/** Adds a new HD chain to the chains list, and make it the default chain (from which keys are issued). */
public void createAndActivateNewHDChain() {
// We can't do auto upgrade here because we don't know the rotation time, if any.
final DeterministicKeyChain chain = new DeterministicKeyChain(new SecureRandom(), DeterministicKeyChain.DEFAULT_SEED_BITS);
final DeterministicKeyChain chain = new DeterministicKeyChain(new SecureRandom());
log.info("Creating and activating a new HD chain: {}", chain);
for (ListenerRegistration<KeyChainEventListener> registration : basic.getListeners())
chain.addEventListener(registration.listener, registration.executor);
@@ -733,12 +733,12 @@ public class KeyChainGroup {
log.info("Auto-upgrading pre-HD wallet using oldest non-rotating private key");
byte[] seed = checkNotNull(keyToUse.getSecretBytes());
// Private keys should be at least 128 bits long.
checkState(seed.length >= DeterministicKeyChain.DEFAULT_SEED_BITS / 8);
checkState(seed.length >= DeterministicSeed.DEFAULT_SEED_ENTROPY_BITS / 8);
// We reduce the entropy here to 128 bits because people like to write their seeds down on paper, and 128
// bits should be sufficient forever unless the laws of the universe change or ECC is broken; in either case
// we all have bigger problems.
seed = Arrays.copyOfRange(seed, 0, DeterministicKeyChain.DEFAULT_SEED_BITS / 8); // final argument is exclusive range.
checkState(seed.length == DeterministicKeyChain.DEFAULT_SEED_BITS / 8);
seed = Arrays.copyOfRange(seed, 0, DeterministicSeed.DEFAULT_SEED_ENTROPY_BITS / 8); // final argument is exclusive range.
checkState(seed.length == DeterministicSeed.DEFAULT_SEED_ENTROPY_BITS / 8);
DeterministicKeyChain chain = new DeterministicKeyChain(seed, keyToUse.getCreationTimeSeconds());
if (aesKey != null) {
chain = chain.toEncrypted(checkNotNull(basic.getKeyCrypter()), aesKey);
@@ -807,7 +807,7 @@ public class KeyChainGroup {
if (seed.isEncrypted()) {
builder2.append(String.format("Seed is encrypted%n"));
} else if (includePrivateKeys) {
final List<String> words = seed.toMnemonicCode();
final List<String> words = seed.getMnemonicCode();
builder2.append(
String.format("Seed as words: %s%nSeed as hex: %s%n", Joiner.on(' ').join(words),
seed.toHexString())

View File

@@ -2271,7 +2271,7 @@ public final class Protos {
*
* <pre>
* The public EC key derived from the private key. We allow both to be stored to avoid mobile clients having to
* do lots of slow EC math on startup. For DETERMINISTIC_ROOT_SEED entries this is missing.
* do lots of slow EC math on startup. For DETERMINISTIC_ROOT_SEED and DETERMINISTIC_MNEMONIC entries this is missing.
* </pre>
*/
boolean hasPublicKey();
@@ -2280,7 +2280,7 @@ public final class Protos {
*
* <pre>
* The public EC key derived from the private key. We allow both to be stored to avoid mobile clients having to
* do lots of slow EC math on startup. For DETERMINISTIC_ROOT_SEED entries this is missing.
* do lots of slow EC math on startup. For DETERMINISTIC_ROOT_SEED and DETERMINISTIC_MNEMONIC entries this is missing.
* </pre>
*/
com.google.protobuf.ByteString getPublicKey();
@@ -2548,6 +2548,17 @@ public final class Protos {
* </pre>
*/
DETERMINISTIC_KEY(3, 4),
/**
* <code>DETERMINISTIC_MNEMONIC = 5;</code>
*
* <pre>
**
* Not really a key, but rather contains the mnemonic phrase for a deterministic key hierarchy in the private_key field.
* The label and public_key fields are missing. Creation timestamp will exist.
* Must be immediately after DETERMINISTIC_KEY
* </pre>
*/
DETERMINISTIC_MNEMONIC(4, 5),
;
/**
@@ -2589,6 +2600,17 @@ public final class Protos {
* </pre>
*/
public static final int DETERMINISTIC_KEY_VALUE = 4;
/**
* <code>DETERMINISTIC_MNEMONIC = 5;</code>
*
* <pre>
**
* Not really a key, but rather contains the mnemonic phrase for a deterministic key hierarchy in the private_key field.
* The label and public_key fields are missing. Creation timestamp will exist.
* Must be immediately after DETERMINISTIC_KEY
* </pre>
*/
public static final int DETERMINISTIC_MNEMONIC_VALUE = 5;
public final int getNumber() { return value; }
@@ -2599,6 +2621,7 @@ public final class Protos {
case 2: return ENCRYPTED_SCRYPT_AES;
case 3: return DETERMINISTIC_ROOT_SEED;
case 4: return DETERMINISTIC_KEY;
case 5: return DETERMINISTIC_MNEMONIC;
default: return null;
}
}
@@ -2735,7 +2758,7 @@ public final class Protos {
*
* <pre>
* The public EC key derived from the private key. We allow both to be stored to avoid mobile clients having to
* do lots of slow EC math on startup. For DETERMINISTIC_ROOT_SEED entries this is missing.
* do lots of slow EC math on startup. For DETERMINISTIC_ROOT_SEED and DETERMINISTIC_MNEMONIC entries this is missing.
* </pre>
*/
public boolean hasPublicKey() {
@@ -2746,7 +2769,7 @@ public final class Protos {
*
* <pre>
* The public EC key derived from the private key. We allow both to be stored to avoid mobile clients having to
* do lots of slow EC math on startup. For DETERMINISTIC_ROOT_SEED entries this is missing.
* do lots of slow EC math on startup. For DETERMINISTIC_ROOT_SEED and DETERMINISTIC_MNEMONIC entries this is missing.
* </pre>
*/
public com.google.protobuf.ByteString getPublicKey() {
@@ -3500,7 +3523,7 @@ public final class Protos {
*
* <pre>
* The public EC key derived from the private key. We allow both to be stored to avoid mobile clients having to
* do lots of slow EC math on startup. For DETERMINISTIC_ROOT_SEED entries this is missing.
* do lots of slow EC math on startup. For DETERMINISTIC_ROOT_SEED and DETERMINISTIC_MNEMONIC entries this is missing.
* </pre>
*/
public boolean hasPublicKey() {
@@ -3511,7 +3534,7 @@ public final class Protos {
*
* <pre>
* The public EC key derived from the private key. We allow both to be stored to avoid mobile clients having to
* do lots of slow EC math on startup. For DETERMINISTIC_ROOT_SEED entries this is missing.
* do lots of slow EC math on startup. For DETERMINISTIC_ROOT_SEED and DETERMINISTIC_MNEMONIC entries this is missing.
* </pre>
*/
public com.google.protobuf.ByteString getPublicKey() {
@@ -3522,7 +3545,7 @@ public final class Protos {
*
* <pre>
* The public EC key derived from the private key. We allow both to be stored to avoid mobile clients having to
* do lots of slow EC math on startup. For DETERMINISTIC_ROOT_SEED entries this is missing.
* do lots of slow EC math on startup. For DETERMINISTIC_ROOT_SEED and DETERMINISTIC_MNEMONIC entries this is missing.
* </pre>
*/
public Builder setPublicKey(com.google.protobuf.ByteString value) {
@@ -3539,7 +3562,7 @@ public final class Protos {
*
* <pre>
* The public EC key derived from the private key. We allow both to be stored to avoid mobile clients having to
* do lots of slow EC math on startup. For DETERMINISTIC_ROOT_SEED entries this is missing.
* do lots of slow EC math on startup. For DETERMINISTIC_ROOT_SEED and DETERMINISTIC_MNEMONIC entries this is missing.
* </pre>
*/
public Builder clearPublicKey() {
@@ -16161,66 +16184,67 @@ public final class Protos {
"ey\030\002 \002(\014\"y\n\020DeterministicKey\022\022\n\nchain_co" +
"de\030\001 \002(\014\022\014\n\004path\030\002 \003(\r\022\026\n\016issued_subkeys" +
"\030\003 \001(\r\022\026\n\016lookahead_size\030\004 \001(\r\022\023\n\013isFoll" +
"owing\030\005 \001(\010\"\302\002\n\003Key\022\036\n\004type\030\001 \002(\0162\020.wall" +
"owing\030\005 \001(\010\"\336\002\n\003Key\022\036\n\004type\030\001 \002(\0162\020.wall" +
"et.Key.Type\022\024\n\014secret_bytes\030\002 \001(\014\022-\n\016enc" +
"rypted_data\030\006 \001(\0132\025.wallet.EncryptedData",
"\022\022\n\npublic_key\030\003 \001(\014\022\r\n\005label\030\004 \001(\t\022\032\n\022c" +
"reation_timestamp\030\005 \001(\003\0223\n\021deterministic" +
"_key\030\007 \001(\0132\030.wallet.DeterministicKey\"b\n\004" +
"_key\030\007 \001(\0132\030.wallet.DeterministicKey\"~\n\004" +
"Type\022\014\n\010ORIGINAL\020\001\022\030\n\024ENCRYPTED_SCRYPT_A" +
"ES\020\002\022\033\n\027DETERMINISTIC_ROOT_SEED\020\003\022\025\n\021DET" +
"ERMINISTIC_KEY\020\004\"5\n\006Script\022\017\n\007program\030\001 " +
"\002(\014\022\032\n\022creation_timestamp\030\002 \002(\003\"\222\001\n\020Tran" +
"sactionInput\022\"\n\032transaction_out_point_ha" +
"sh\030\001 \002(\014\022#\n\033transaction_out_point_index\030" +
"\002 \002(\r\022\024\n\014script_bytes\030\003 \002(\014\022\020\n\010sequence\030",
"\004 \001(\r\022\r\n\005value\030\005 \001(\003\"\177\n\021TransactionOutpu" +
"t\022\r\n\005value\030\001 \002(\003\022\024\n\014script_bytes\030\002 \002(\014\022!" +
"\n\031spent_by_transaction_hash\030\003 \001(\014\022\"\n\032spe" +
"nt_by_transaction_index\030\004 \001(\005\"\234\003\n\025Transa" +
"ctionConfidence\0220\n\004type\030\001 \001(\0162\".wallet.T" +
"ransactionConfidence.Type\022\032\n\022appeared_at" +
"_height\030\002 \001(\005\022\036\n\026overriding_transaction\030" +
"\003 \001(\014\022\r\n\005depth\030\004 \001(\005\022\021\n\twork_done\030\005 \001(\003\022" +
")\n\014broadcast_by\030\006 \003(\0132\023.wallet.PeerAddre" +
"ss\0224\n\006source\030\007 \001(\0162$.wallet.TransactionC",
"onfidence.Source\"O\n\004Type\022\013\n\007UNKNOWN\020\000\022\014\n" +
"\010BUILDING\020\001\022\013\n\007PENDING\020\002\022\025\n\021NOT_IN_BEST_" +
"CHAIN\020\003\022\010\n\004DEAD\020\004\"A\n\006Source\022\022\n\016SOURCE_UN" +
"KNOWN\020\000\022\022\n\016SOURCE_NETWORK\020\001\022\017\n\013SOURCE_SE" +
"LF\020\002\"\236\004\n\013Transaction\022\017\n\007version\030\001 \002(\005\022\014\n" +
"\004hash\030\002 \002(\014\022&\n\004pool\030\003 \001(\0162\030.wallet.Trans" +
"action.Pool\022\021\n\tlock_time\030\004 \001(\r\022\022\n\nupdate" +
"d_at\030\005 \001(\003\0223\n\021transaction_input\030\006 \003(\0132\030." +
"wallet.TransactionInput\0225\n\022transaction_o" +
"utput\030\007 \003(\0132\031.wallet.TransactionOutput\022\022",
"\n\nblock_hash\030\010 \003(\014\022 \n\030block_relativity_o" +
"ffsets\030\013 \003(\005\0221\n\nconfidence\030\t \001(\0132\035.walle" +
"t.TransactionConfidence\0225\n\007purpose\030\n \001(\016" +
"2\033.wallet.Transaction.Purpose:\007UNKNOWN\"Y" +
"\n\004Pool\022\013\n\007UNSPENT\020\004\022\t\n\005SPENT\020\005\022\014\n\010INACTI" +
"VE\020\002\022\010\n\004DEAD\020\n\022\013\n\007PENDING\020\020\022\024\n\020PENDING_I" +
"NACTIVE\020\022\":\n\007Purpose\022\013\n\007UNKNOWN\020\000\022\020\n\014USE" +
"R_PAYMENT\020\001\022\020\n\014KEY_ROTATION\020\002\"N\n\020ScryptP" +
"arameters\022\014\n\004salt\030\001 \002(\014\022\020\n\001n\030\002 \001(\003:\0051638" +
"4\022\014\n\001r\030\003 \001(\005:\0018\022\014\n\001p\030\004 \001(\005:\0011\"8\n\tExtensi",
"on\022\n\n\002id\030\001 \002(\t\022\014\n\004data\030\002 \002(\014\022\021\n\tmandator" +
"y\030\003 \002(\010\" \n\003Tag\022\013\n\003tag\030\001 \002(\t\022\014\n\004data\030\002 \002(" +
"\014\"\261\004\n\006Wallet\022\032\n\022network_identifier\030\001 \002(\t" +
"\022\034\n\024last_seen_block_hash\030\002 \001(\014\022\036\n\026last_s" +
"een_block_height\030\014 \001(\r\022!\n\031last_seen_bloc" +
"k_time_secs\030\016 \001(\003\022\030\n\003key\030\003 \003(\0132\013.wallet." +
"Key\022(\n\013transaction\030\004 \003(\0132\023.wallet.Transa" +
"ction\022&\n\016watched_script\030\017 \003(\0132\016.wallet.S" +
"cript\022C\n\017encryption_type\030\005 \001(\0162\035.wallet." +
"Wallet.EncryptionType:\013UNENCRYPTED\0227\n\025en",
"cryption_parameters\030\006 \001(\0132\030.wallet.Scryp" +
"tParameters\022\022\n\007version\030\007 \001(\005:\0011\022$\n\texten" +
"sion\030\n \003(\0132\021.wallet.Extension\022\023\n\013descrip" +
"tion\030\013 \001(\t\022\031\n\021key_rotation_time\030\r \001(\004\022\031\n" +
"\004tags\030\020 \003(\0132\013.wallet.Tag\";\n\016EncryptionTy" +
"pe\022\017\n\013UNENCRYPTED\020\001\022\030\n\024ENCRYPTED_SCRYPT_" +
"AES\020\002B\035\n\023org.bitcoinj.walletB\006Protos"
"ERMINISTIC_KEY\020\004\022\032\n\026DETERMINISTIC_MNEMON" +
"IC\020\005\"5\n\006Script\022\017\n\007program\030\001 \002(\014\022\032\n\022creat" +
"ion_timestamp\030\002 \002(\003\"\222\001\n\020TransactionInput" +
"\022\"\n\032transaction_out_point_hash\030\001 \002(\014\022#\n\033" +
"transaction_out_point_index\030\002 \002(\r\022\024\n\014scr",
"ipt_bytes\030\003 \002(\014\022\020\n\010sequence\030\004 \001(\r\022\r\n\005val" +
"ue\030\005 \001(\003\"\177\n\021TransactionOutput\022\r\n\005value\030\001" +
" \002(\003\022\024\n\014script_bytes\030\002 \002(\014\022!\n\031spent_by_t" +
"ransaction_hash\030\003 \001(\014\022\"\n\032spent_by_transa" +
"ction_index\030\004 \001(\005\"\234\003\n\025TransactionConfide" +
"nce\0220\n\004type\030\001 \001(\0162\".wallet.TransactionCo" +
"nfidence.Type\022\032\n\022appeared_at_height\030\002 \001(" +
"\005\022\036\n\026overriding_transaction\030\003 \001(\014\022\r\n\005dep" +
"th\030\004 \001(\005\022\021\n\twork_done\030\005 \001(\003\022)\n\014broadcast" +
"_by\030\006 \003(\0132\023.wallet.PeerAddress\0224\n\006source",
"\030\007 \001(\0162$.wallet.TransactionConfidence.So" +
"urce\"O\n\004Type\022\013\n\007UNKNOWN\020\000\022\014\n\010BUILDING\020\001\022" +
"\013\n\007PENDING\020\002\022\025\n\021NOT_IN_BEST_CHAIN\020\003\022\010\n\004D" +
"EAD\020\004\"A\n\006Source\022\022\n\016SOURCE_UNKNOWN\020\000\022\022\n\016S" +
"OURCE_NETWORK\020\001\022\017\n\013SOURCE_SELF\020\002\"\236\004\n\013Tra" +
"nsaction\022\017\n\007version\030\001 \002(\005\022\014\n\004hash\030\002 \002(\014\022" +
"&\n\004pool\030\003 \001(\0162\030.wallet.Transaction.Pool\022" +
"\021\n\tlock_time\030\004 \001(\r\022\022\n\nupdated_at\030\005 \001(\003\0223" +
"\n\021transaction_input\030\006 \003(\0132\030.wallet.Trans" +
"actionInput\0225\n\022transaction_output\030\007 \003(\0132",
"\031.wallet.TransactionOutput\022\022\n\nblock_hash" +
"\030\010 \003(\014\022 \n\030block_relativity_offsets\030\013 \003(\005" +
"\0221\n\nconfidence\030\t \001(\0132\035.wallet.Transactio" +
"nConfidence\0225\n\007purpose\030\n \001(\0162\033.wallet.Tr" +
"ansaction.Purpose:\007UNKNOWN\"Y\n\004Pool\022\013\n\007UN" +
"SPENT\020\004\022\t\n\005SPENT\020\005\022\014\n\010INACTIVE\020\002\022\010\n\004DEAD" +
"\020\n\022\013\n\007PENDING\020\020\022\024\n\020PENDING_INACTIVE\020\022\":\n" +
"\007Purpose\022\013\n\007UNKNOWN\020\000\022\020\n\014USER_PAYMENT\020\001\022" +
"\020\n\014KEY_ROTATION\020\002\"N\n\020ScryptParameters\022\014\n" +
"\004salt\030\001 \002(\014\022\020\n\001n\030\002 \001(\003:\00516384\022\014\n\001r\030\003 \001(\005",
":\0018\022\014\n\001p\030\004 \001(\005:\0011\"8\n\tExtension\022\n\n\002id\030\001 \002" +
"(\t\022\014\n\004data\030\002 \002(\014\022\021\n\tmandatory\030\003 \002(\010\" \n\003T" +
"ag\022\013\n\003tag\030\001 \002(\t\022\014\n\004data\030\002 \002(\014\"\261\004\n\006Wallet" +
"\022\032\n\022network_identifier\030\001 \002(\t\022\034\n\024last_see" +
"n_block_hash\030\002 \001(\014\022\036\n\026last_seen_block_he" +
"ight\030\014 \001(\r\022!\n\031last_seen_block_time_secs\030" +
"\016 \001(\003\022\030\n\003key\030\003 \003(\0132\013.wallet.Key\022(\n\013trans" +
"action\030\004 \003(\0132\023.wallet.Transaction\022&\n\016wat" +
"ched_script\030\017 \003(\0132\016.wallet.Script\022C\n\017enc" +
"ryption_type\030\005 \001(\0162\035.wallet.Wallet.Encry",
"ptionType:\013UNENCRYPTED\0227\n\025encryption_par" +
"ameters\030\006 \001(\0132\030.wallet.ScryptParameters\022" +
"\022\n\007version\030\007 \001(\005:\0011\022$\n\textension\030\n \003(\0132\021" +
".wallet.Extension\022\023\n\013description\030\013 \001(\t\022\031" +
"\n\021key_rotation_time\030\r \001(\004\022\031\n\004tags\030\020 \003(\0132" +
"\013.wallet.Tag\";\n\016EncryptionType\022\017\n\013UNENCR" +
"YPTED\020\001\022\030\n\024ENCRYPTED_SCRYPT_AES\020\002B\035\n\023org" +
".bitcoinj.walletB\006Protos"
};
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {

View File

@@ -97,7 +97,12 @@ public class WalletTest extends TestWithWallet {
@Test
public void getSeedAsWords1() {
// Can't verify much here as the wallet is random each time. We could fix the RNG for the unit tests and solve.
assertEquals(12, wallet.getKeyChainSeed().toMnemonicCode().size());
assertEquals(12, wallet.getKeyChainSeed().getMnemonicCode().size());
}
@Test
public void checkSeed() throws MnemonicException {
wallet.getKeyChainSeed().check();
}
@Test

View File

@@ -54,14 +54,6 @@ public class DeterministicKeyChainTest {
assertEquals(secs, checkNotNull(chain.getSeed()).getCreationTimeSeconds());
}
@Test
public void mnemonicCode() throws Exception {
final List<String> words = chain.toMnemonicCode();
assertEquals("aerobic toe save section draw warm cute upon raccoon mother priority pilot taste sweet next traffic fatal sword dentist original crisp team caution rebel",
Joiner.on(" ").join(words));
new DeterministicSeed(words, checkNotNull(chain.getSeed()).getCreationTimeSeconds());
}
@Test
public void derive() throws Exception {
ECKey key1 = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);

View File

@@ -380,25 +380,25 @@ public class KeyChainGroupTest {
@Test
public void serialization() throws Exception {
assertEquals(INITIAL_KEYS + 1 /* for the seed */, group.serializeToProtobuf().size());
assertEquals(INITIAL_KEYS + 2 /* for the seed */, group.serializeToProtobuf().size());
group = KeyChainGroup.fromProtobufUnencrypted(params, group.serializeToProtobuf());
group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
DeterministicKey key1 = group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
DeterministicKey key2 = group.freshKey(KeyChain.KeyPurpose.CHANGE);
List<Protos.Key> protoKeys1 = group.serializeToProtobuf();
assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */ + 1, protoKeys1.size());
assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 2 /* for the seed */ + 1, protoKeys1.size());
group.importKeys(new ECKey());
List<Protos.Key> protoKeys2 = group.serializeToProtobuf();
assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */ + 2, protoKeys2.size());
assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 2 /* for the seed */ + 2, protoKeys2.size());
group = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys1);
assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */ + 1, protoKeys1.size());
assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 2 /* for the seed */ + 1, protoKeys1.size());
assertTrue(group.hasKey(key1));
assertTrue(group.hasKey(key2));
assertEquals(key2, group.currentKey(KeyChain.KeyPurpose.CHANGE));
assertEquals(key1, group.currentKey(KeyChain.KeyPurpose.RECEIVE_FUNDS));
group = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys2);
assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */ + 2, protoKeys2.size());
assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 2 /* for the seed */ + 2, protoKeys2.size());
assertTrue(group.hasKey(key1));
assertTrue(group.hasKey(key2));

View File

@@ -99,6 +99,13 @@ message Key {
* key can be rederived on the fly.
*/
DETERMINISTIC_KEY = 4;
/**
* Not really a key, but rather contains the mnemonic phrase for a deterministic key hierarchy in the private_key field.
* The label and public_key fields are missing. Creation timestamp will exist.
* Must be immediately after DETERMINISTIC_KEY
*/
DETERMINISTIC_MNEMONIC = 5;
}
required Type type = 1;
@@ -109,7 +116,7 @@ message Key {
optional EncryptedData encrypted_data = 6;
// The public EC key derived from the private key. We allow both to be stored to avoid mobile clients having to
// do lots of slow EC math on startup. For DETERMINISTIC_ROOT_SEED entries this is missing.
// do lots of slow EC math on startup. For DETERMINISTIC_ROOT_SEED and DETERMINISTIC_MNEMONIC entries this is missing.
optional bytes public_key = 3;
// User-provided label associated with the key.

View File

@@ -853,8 +853,10 @@ public class WalletTool {
if (seedStr.contains(" ")) {
// Parse as mnemonic code.
final List<String> split = ImmutableList.copyOf(Splitter.on(" ").omitEmptyStrings().split(seedStr));
String passphrase = ""; // TODO allow user to specify a passphrase
seed = new DeterministicSeed(split, passphrase, creationTimeSecs);
try {
seed = new DeterministicSeed(split, creationTimeSecs);
seed.check();
} catch (MnemonicException.MnemonicLengthException e) {
System.err.println("The seed did not have 12 words in, perhaps you need quotes around it?");
return;
@@ -864,6 +866,9 @@ public class WalletTool {
} catch (MnemonicException.MnemonicChecksumException e) {
System.err.println("The seed did not pass checksumming, perhaps one of the words is wrong?");
return;
} catch (MnemonicException e) {
// not reached - all subclasses handled above
throw new RuntimeException(e);
}
} else {
// Parse as hex or base58

View File

@@ -12,4 +12,4 @@ if [ ! -e target/bitcoinj-tools-*.jar ] || [[ "$ALWAYS_BUILD_WALLETTOOL" != "" ]
mvn package -DskipTests
fi
java -jar target/bitcoinj-tools-*.jar $*
java -jar target/bitcoinj-tools-*.jar "$@"