Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-enable HD wallet #231

Merged
merged 5 commits into from
Jul 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 19 additions & 20 deletions src/main/java/org/semux/cli/SemuxCli.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import org.semux.core.exception.WalletLockedException;
import org.semux.crypto.Hex;
import org.semux.crypto.Key;
import org.semux.crypto.bip39.Language;
import org.semux.crypto.bip39.MnemonicGenerator;
import org.semux.exception.LauncherException;
import org.semux.message.CliMessages;
Expand All @@ -41,7 +40,7 @@
*/
public class SemuxCli extends Launcher {

public static final boolean HD_WALLET_ENABLED = false;
public static final boolean HD_WALLET_ENABLED = true;

private static final Logger logger = LoggerFactory.getLogger(SemuxCli.class);

Expand Down Expand Up @@ -165,13 +164,6 @@ protected void start() throws IOException {
return;
}

// in case HD wallet is enabled, make sure the seed is properly initialized.
if (HD_WALLET_ENABLED) {
if (!wallet.isHdWalletInitialized()) {
initializedHdSeed(wallet);
}
}

// check file permissions
if (SystemUtil.isPosix()) {
if (!wallet.isPosixPermissionSecured()) {
Expand All @@ -181,10 +173,17 @@ protected void start() throws IOException {

// check time drift
long timeDrift = TimeUtil.getTimeOffsetFromNtp();
if (Math.abs(timeDrift) > 20000L) {
if (Math.abs(timeDrift) > 5000L) {
logger.warn(CliMessages.get("SystemTimeDrift"));
}

// in case HD wallet is enabled, make sure the seed is properly initialized.
if (HD_WALLET_ENABLED) {
if (!wallet.isHdWalletInitialized()) {
initializedHdSeed(wallet);
}
}

// create a new account if the wallet is empty
List<Key> accounts = wallet.getAccounts();
if (accounts.isEmpty()) {
Expand Down Expand Up @@ -405,18 +404,18 @@ protected void initializedHdSeed(Wallet wallet) {
if (wallet.isUnlocked() && !wallet.isHdWalletInitialized()) {
// HD Mnemonic
MnemonicGenerator generator = new MnemonicGenerator();
String phrase = generator.getWordlist(128, Language.ENGLISH);
System.out.println(CliMessages.get("HdWalletInstructions"));
System.out.println(phrase);

// HD Password
String passCode = readNewPassword("EnterNewHdPassword", "ReEnterNewHdPassword");
String phrase = generator.getWordlist(Wallet.MNEMONIC_ENTROPY_LENGTH, Wallet.MNEMONIC_LANGUAGE);
System.out.println(CliMessages.get("HdWalletInstructions", phrase));

// HD seed based on the mnemonics and password
byte[] seed = generator.getSeedFromWordlist(phrase, passCode, Language.ENGLISH);
String repeat = ConsoleUtil.readLine(CliMessages.get("HdWalletMnemonicRepeat"));
if (!repeat.equals(phrase)) {
logger.info(CliMessages.get("HdWalletInitializationSuccess"));
SystemUtil.exit(SystemUtil.Code.FAILED_TO_INIT_HD_WALLET);
} else {
logger.error(CliMessages.get("HdWalletInitializationFailure"));
}

wallet.setHdSeed(seed);
wallet.scanForHdKeys(null);
wallet.initializeHdWallet(phrase);
wallet.flush();
}
}
Expand Down
170 changes: 103 additions & 67 deletions src/main/java/org/semux/core/Wallet.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import java.util.concurrent.ConcurrentHashMap;

import org.bouncycastle.crypto.generators.BCrypt;
import org.semux.Network;
import org.semux.core.exception.WalletLockedException;
import org.semux.core.state.Account;
import org.semux.core.state.AccountState;
Expand All @@ -35,8 +34,10 @@
import org.semux.crypto.bip32.CoinType;
import org.semux.crypto.bip32.HdKeyPair;
import org.semux.crypto.bip32.key.KeyVersion;
import org.semux.crypto.bip39.Language;
import org.semux.crypto.bip39.MnemonicGenerator;
import org.semux.crypto.bip44.Bip44;
import org.semux.message.GuiMessages;
import org.semux.message.CliMessages;
import org.semux.util.ByteArray;
import org.semux.util.Bytes;
import org.semux.util.FileUtil;
Expand All @@ -55,23 +56,29 @@ public class Wallet {
private static final int SALT_LENGTH = 16;
private static final int BCRYPT_COST = 12;
private static final Bip44 BIP_44 = new Bip44();
private static final int MAX_HD_WALLET_SCAN_AHEAD = 20;
private static final int MAX_HD_WALLET_SCAN_AHEAD = 64;

// the BIP-44 path prefix for semux addresses
private static final String PATH_PREFIX = "m/44'/7562605'/0'/0'";
public static final String PATH_PREFIX = "m/44'/7562605'/0'/0'";
public static final String MNEMONIC_PASS_PHRASE = "";
public static final Language MNEMONIC_LANGUAGE = Language.ENGLISH;
public static final int MNEMONIC_ENTROPY_LENGTH = 128;
// always use mainnet to avoid confusion, since the generated key is stored
public static final KeyVersion KEY_VERSION = KeyVersion.MAINNET;
public static final CoinType COIN_TYPE = CoinType.SEMUX_SLIP10; // TODO: consider BIP32-ED25519

private final File file;
private final org.semux.Network network;

// hd wallet key
private byte[] hdSeed = new byte[0];
private int nextHdAccountIndex = 0;

private final Map<ByteArray, Key> accounts = Collections.synchronizedMap(new LinkedHashMap<>());
private final Map<ByteArray, String> aliases = new ConcurrentHashMap<>();

private String password;

// hd wallet key
private String mnemonicPhrase = "";
private int nextAccountIndex = 0;

/**
* Creates a new wallet instance.
*
Expand Down Expand Up @@ -153,8 +160,7 @@ public boolean unlock(String password) {
key = BCrypt.generate(Bytes.of(password), salt, BCRYPT_COST);
newAccounts = readAccounts(key, dec, true, version);
newAliases = readAddressAliases(key, dec);
hdSeed = dec.readBytes();
nextHdAccountIndex = dec.readInt();
readHdSeed(key, dec);
break;
default:
throw new CryptoException("Unknown wallet version.");
Expand Down Expand Up @@ -184,17 +190,6 @@ public boolean unlock(String password) {
return false;
}

private KeyVersion getKeyVersion(Network network) {
switch (network) {
case DEVNET:
case TESTNET:
return KeyVersion.TESTNET;
case MAINNET:
return KeyVersion.MAINNET;
}
throw new IllegalArgumentException("Unknown network.");
}

/**
* Reads the account keys.
*
Expand Down Expand Up @@ -240,7 +235,8 @@ protected void writeAccounts(byte[] key, SimpleEncoder enc) {

/**
* Reads the address book.
*
*
* @param key
* @param dec
* @return
*/
Expand All @@ -263,7 +259,8 @@ protected Map<ByteArray, String> readAddressAliases(byte[] key, SimpleDecoder de

/**
* Writes the address book.
*
*
* @param key
* @param enc
*/
protected void writeAddressAliases(byte[] key, SimpleEncoder enc) {
Expand All @@ -284,6 +281,42 @@ protected void writeAddressAliases(byte[] key, SimpleEncoder enc) {
enc.writeBytes(aliasesEncrypted);
}

/**
* Reads the mnemonic phase and next account index.
*
* @param key
* @param dec
* @return
*/
protected void readHdSeed(byte[] key, SimpleDecoder dec) {
byte[] iv = dec.readBytes();
byte[] hdSeedEncrypted = dec.readBytes();
byte[] hdSeedRaw = Aes.decrypt(hdSeedEncrypted, key, iv);

SimpleDecoder d = new SimpleDecoder(hdSeedRaw);
mnemonicPhrase = d.readString();
nextAccountIndex = d.readInt();
}

/**
* Writes the mnemonic phase and next account index.
*
* @param key
* @param enc
*/
protected void writeHdSeed(byte[] key, SimpleEncoder enc) {
SimpleEncoder e = new SimpleEncoder();
e.writeString(mnemonicPhrase);
e.writeInt(nextAccountIndex);

byte[] iv = Bytes.random(16);
byte[] hdSeedRaw = e.toBytes();
byte[] hdSeedEncrypted = Aes.encrypt(hdSeedRaw, key, iv);

enc.writeBytes(iv);
enc.writeBytes(hdSeedEncrypted);
}

/**
* Locks the wallet.
*/
Expand Down Expand Up @@ -412,12 +445,12 @@ public boolean addAccount(Key newKey) throws WalletLockedException {
requireUnlocked();

synchronized (accounts) {
ByteArray to = ByteArray.of(newKey.toAddress());
if (accounts.containsKey(to)) {
ByteArray address = ByteArray.of(newKey.toAddress());
if (accounts.containsKey(address)) {
return false;
}

accounts.put(to, newKey);
accounts.put(address, newKey);
return true;
}
}
Expand Down Expand Up @@ -488,7 +521,6 @@ public boolean removeAccount(byte[] address) throws WalletLockedException {
if (removed && isHdWalletInitialized()) {
// remove the alias for the account
removeAddressAlias(address);
scanForHdKeys(null);
}
return removed;
}
Expand Down Expand Up @@ -534,9 +566,7 @@ public boolean flush() throws WalletLockedException {

writeAccounts(key, enc);
writeAddressAliases(key, enc);

enc.writeBytes(hdSeed);
enc.writeInt(nextHdAccountIndex);
writeHdSeed(key, enc);

if (!file.getParentFile().exists() && !file.getParentFile().mkdirs()) {
logger.error("Failed to create the directory for wallet");
Expand Down Expand Up @@ -664,18 +694,28 @@ private void requireUnlocked() throws WalletLockedException {
*/
public boolean isHdWalletInitialized() {
requireUnlocked();
return hdSeed != null && hdSeed.length > 0;
return mnemonicPhrase != null && !mnemonicPhrase.isEmpty();
}

/**
* Initialize the HD wallet.
*
* @param mnemonicPhrase
* the mnemonic word list
*/
public void initializeHdWallet(String mnemonicPhrase) {
this.mnemonicPhrase = mnemonicPhrase;
this.nextAccountIndex = 0;
}

/**
* Sets the HD seed
* Returns the HD seed.
*
* @param seed
* the seed byte array
* @return seed
*/
public void setHdSeed(byte[] seed) {
this.hdSeed = seed;
this.nextHdAccountIndex = 0;
public byte[] getSeed() {
MnemonicGenerator generator = new MnemonicGenerator();
return generator.getSeedFromWordlist(mnemonicPhrase, MNEMONIC_PASS_PHRASE, MNEMONIC_LANGUAGE);
}

/**
Expand All @@ -689,66 +729,62 @@ public Key addAccountWithNextHdKey() {
requireHdWalletInitialized();

synchronized (accounts) {
HdKeyPair rootKey = BIP_44.getRootKeyPairFromSeed(hdSeed, KeyVersion.MAINNET, CoinType.SEMUX_SLIP10);
HdKeyPair key = BIP_44.getChildKeyPair(rootKey, nextHdAccountIndex++);
Key newKey = Key.fromRawPrivateKey(key.getPrivateKey().getKeyData());
ByteArray to = ByteArray.of(newKey.toAddress());
byte[] seed = getSeed();
HdKeyPair rootKey = BIP_44.getRootKeyPairFromSeed(seed, KEY_VERSION, COIN_TYPE);
HdKeyPair childKey = BIP_44.getChildKeyPair(rootKey, nextAccountIndex++);

Key key = Key.fromRawPrivateKey(childKey.getPrivateKey().getKeyData());
ByteArray address = ByteArray.of(key.toAddress());

// put the accounts into
accounts.put(to, newKey);
accounts.put(address, key);

// set a default alias
if (!aliases.containsKey(to)) {
setAddressAlias(newKey.toAddress(), getAliasFromPath(key.getPath()));
if (!aliases.containsKey(address)) {
setAddressAlias(address.getData(), getAliasFromPath(childKey.getPath()));
}

return newKey;
return key;
}
}

/**
* Scan for HD keys used accounts, and add them to the account.
* Scans for used HD keys from the 0-th index.
*
* Add any found used keys to this wallet.
*
* Increase the `nextAccountIndex` if a larger index is discovered.
*
* @return the number of accounts found
*/
public int scanForHdKeys(AccountState accountState) {
requireUnlocked();
requireHdWalletInitialized();

int found = 0;

// make sure to add at least the default account
if (nextHdAccountIndex == 0) {
addAccountWithNextHdKey();
found++;
}
HdKeyPair rootAddress = BIP_44.getRootKeyPairFromSeed(getSeed(), KEY_VERSION, COIN_TYPE);

HdKeyPair rootAddress = BIP_44.getRootKeyPairFromSeed(hdSeed, getKeyVersion(network), CoinType.SEMUX_SLIP10);

nextHdAccountIndex = 0;

int start = nextHdAccountIndex;
int start = 0;
int endIndex = start + MAX_HD_WALLET_SCAN_AHEAD;

int found = 0;
for (int i = start; i < endIndex; i++) {
HdKeyPair childKey = BIP_44.getChildKeyPair(rootAddress, i);

Key key = Key.fromRawPrivateKey(childKey.getPrivateKey().getKeyData());
ByteArray address = ByteArray.of(key.toAddress());

boolean isUsedAccount = isUsedAccount(accountState, key.toAddress());

ByteArray to = ByteArray.of(key.toAddress());
// if we find an account that has been used, we push forward our end search.
// an account exists if its in our wallet, has balance, or has made transactions
if (isUsedAccount || accounts.containsKey(to)) {
if (isUsedAccount || accounts.containsKey(address)) {
endIndex += MAX_HD_WALLET_SCAN_AHEAD;
if (addAccount(key)) {
if (!aliases.containsKey(to)) {
aliases.put(to, childKey.getPath());
if (!aliases.containsKey(address)) {
setAddressAlias(address.getData(), getAliasFromPath(childKey.getPath()));
}
found++;
}
if (i >= nextHdAccountIndex) {
nextHdAccountIndex = i + 1;
if (i >= nextAccountIndex) {
nextAccountIndex = i + 1;
}
}
}
Expand All @@ -769,7 +805,7 @@ public int scanForHdKeys(AccountState accountState) {
* @return
*/
private String getAliasFromPath(String path) {
return path.replace(PATH_PREFIX, GuiMessages.get("HdWalletAliasPrefix"));
return path.replace(PATH_PREFIX, CliMessages.get("HdWalletAliasPrefix"));
}

private boolean isUsedAccount(AccountState accountState, byte[] bytes) {
Expand Down
Loading