diff --git a/README.md b/README.md index 8051513..730613c 100644 --- a/README.md +++ b/README.md @@ -1,134 +1,184 @@ -

Tokencore

- -

- Multi-chain cryptocurrency wallet core library for Java -

- -

- - Build Status - - - JitPack - -

- -

- Quick Start (30s)  •  - Integration  •  - Recommended Minimum  •  - Supported Chains -

+# Tokencore + +Tokencore is a Java multi-chain wallet core library for exchange backends, custody systems, and wallet services. + +## What Tokencore provides + +- Multi-chain address generation +- HD wallet derivation and mnemonic workflows +- Encrypted keystore management +- Offline transaction signing for major chain families + +Supported chains include: +- **EVM**: Ethereum +- **Bitcoin family**: Bitcoin, Litecoin, Dogecoin, Dash, Bitcoin Cash, Bitcoin SV +- **Others**: TRON, Filecoin, EOS --- -## Introduction +## Core Features (Recommended Minimum) -Tokencore is a lightweight Java library for wallet fundamentals: HD derivation, encrypted keystore management, and offline signing. +- Java 8+ +- Gradle wrapper included (`./gradlew`) -If your goal is "install and use immediately", start with the 30-second quick start below and only enable additional chains/features later. +--- -## Quick Start (30 seconds) +## Install -### 1) Add dependency +### Gradle ```gradle repositories { maven { url 'https://jitpack.io' } } + dependencies { implementation 'com.github.galaxyscitech:tokencore:1.3.0' } ``` -### 2) Copy this minimal bootstrap code +### Maven -```java -WalletManager.storage = () -> new File("./keystore"); -WalletManager.scanWallets(); +```xml + + + jitpack.io + https://jitpack.io + + -String password = "change_me"; -Identity identity = Identity.getCurrentIdentity(); -if (identity == null) { - identity = Identity.createIdentity("default", password, "", Network.MAINNET, Metadata.P2WPKH); -} + + com.github.galaxyscitech + tokencore + 1.3.0 + +``` + +--- -Wallet wallet = identity.deriveWalletByMnemonics( - ChainType.ETHEREUM, - password, - MnemonicUtil.randomMnemonicCodes()); +## Quick start (runnable) -System.out.println("Address = " + wallet.getAddress()); +```java +import org.consenlabs.tokencore.foundation.utils.MnemonicUtil; +import org.consenlabs.tokencore.wallet.*; +import org.consenlabs.tokencore.wallet.model.*; + +import java.io.File; + +public class QuickStart { + public static void main(String[] args) { + WalletManager.storage = () -> new File("./keystore"); + WalletManager.scanWallets(); + + String password = "UseAStrongPassword_123"; + Identity identity = Identity.getCurrentIdentity(); + if (identity == null) { + identity = Identity.createIdentity("main", password, "", Network.MAINNET, Metadata.P2WPKH); + } + + Wallet ethWallet = identity.deriveWalletByMnemonics( + ChainType.ETHEREUM, + password, + MnemonicUtil.randomMnemonicCodes() + ); + + Wallet btcWallet = identity.deriveWalletByMnemonics( + ChainType.BITCOIN, + password, + MnemonicUtil.randomMnemonicCodes() + ); + + System.out.println("ETH address: " + ethWallet.getAddress()); + System.out.println("BTC address: " + btcWallet.getAddress()); + } +} ``` -### 3) Verify locally +--- -```bash -./gradlew test -``` +## Common usage -## Core Features (Recommended Minimum) +### 1) Import wallet from private key -For new integrators, keep the initial rollout small: +```java +Metadata metadata = new Metadata(); +metadata.setChainType(ChainType.ETHEREUM); +metadata.setSource(Metadata.FROM_PRIVATE); +metadata.setNetwork(Network.MAINNET); + +Wallet wallet = WalletManager.importWalletFromPrivateKey( + metadata, + "4c0883a69102937d6231471b5dbb6204fe512961708279f14a15c89a7e5a5c3c", + "password123", + true +); +``` -1. **Identity + keystore only** (account generation + secure storage) -2. **Single chain first** (recommend: ETH or BTC) -3. **Offline signing only** (avoid online key usage) -4. **No multi-chain abstraction in v1 API surface** +### 2) Import wallet from mnemonic -This reduces integration complexity and speeds up first successful deployment. +```java +Metadata metadata = new Metadata(); +metadata.setChainType(ChainType.DOGECOIN); +metadata.setSource(Metadata.FROM_MNEMONIC); +metadata.setNetwork(Network.MAINNET); +metadata.setSegWit(Metadata.NONE); + +Wallet wallet = WalletManager.importWalletFromMnemonic( + metadata, + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + BIP44Util.DOGECOIN_MAINNET_PATH, + "password123", + true +); +``` -## Integration +### 3) Find wallet by mnemonic (BTC-family friendly) -### Gradle +```java +Wallet wallet = WalletManager.findWalletByMnemonic( + ChainType.DOGECOIN, + Network.MAINNET, + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + BIP44Util.DOGECOIN_MAINNET_PATH, + Metadata.NONE +); +``` -```gradle -repositories { - maven { url 'https://jitpack.io' } -} -dependencies { - implementation 'com.github.galaxyscitech:tokencore:1.3.0' -} +### 4) Export keystore and recover by keystore + +```java +String keystoreJson = WalletManager.exportKeystore(wallet.getId(), "password123"); +Wallet found = WalletManager.findWalletByKeystore(ChainType.ETHEREUM, keystoreJson, "password123"); ``` -### Maven +--- -```xml - - - jitpack.io - https://jitpack.io - - +## Security recommendations - - com.github.galaxyscitech - tokencore - 1.3.0 - -``` +- Never log or print private keys, mnemonics, or decrypted keystore payloads. +- Keep signing in isolated/offline environments whenever possible. +- Use strong passwords and avoid hardcoded secrets. +- Consider HSM/KMS for production secret governance. +- Enforce strict access controls around keystore files. -## Supported Chains +--- + +## Typical errors -| Chain | Token Standards | Features | -|-------|----------------|----------| -| **Bitcoin** | BTC, OMNI | UTXO management, SegWit (P2WPKH) | -| **Ethereum** | ETH, ERC-20 | Offline signing, nonce management | -| **TRON** | TRX, TRC-20 | Transaction signing | -| **Bitcoin Cash** | BCH | CashAddr format | -| **Bitcoin SV** | BSV | Transaction signing | -| **Litecoin** | LTC | Transaction signing | -| **Dogecoin** | DOGE | Transaction signing | -| **Dash** | DASH | Transaction signing | -| **Filecoin** | FIL | Transaction signing | +- `password_incorrect` +- `mnemonic_length_invalid` +- `mnemonic_word_invalid` +- `invalid_mnemonic_path` +- `unsupported_chain` +- `private_key_address_not_match` + +--- -## Build & Test +## Build and test ```bash -./gradlew build ./gradlew test +./gradlew build ``` -## License - -This project is licensed under the [GNU General Public License v3.0](LICENSE). +CI runs on Java 8/11/17 via GitHub Actions. diff --git a/src/main/java/org/consenlabs/tokencore/foundation/utils/MnemonicUtil.java b/src/main/java/org/consenlabs/tokencore/foundation/utils/MnemonicUtil.java index b5de055..d9c52a5 100755 --- a/src/main/java/org/consenlabs/tokencore/foundation/utils/MnemonicUtil.java +++ b/src/main/java/org/consenlabs/tokencore/foundation/utils/MnemonicUtil.java @@ -1,43 +1,59 @@ -package org.consenlabs.tokencore.foundation.utils; - -import com.google.common.base.Joiner; -import org.bitcoinj.crypto.MnemonicCode; -import org.consenlabs.tokencore.wallet.model.Messages; -import org.consenlabs.tokencore.wallet.model.TokenException; - -import java.util.List; - -public class MnemonicUtil { - public static void validateMnemonics(List mnemonicCodes) { - try { - MnemonicCode.INSTANCE.check(mnemonicCodes); - } catch (org.bitcoinj.crypto.MnemonicException.MnemonicLengthException e) { - throw new TokenException(Messages.MNEMONIC_INVALID_LENGTH); - } catch (org.bitcoinj.crypto.MnemonicException.MnemonicWordException e) { - throw new TokenException(Messages.MNEMONIC_BAD_WORD); - } catch (Exception e) { - throw new TokenException(Messages.MNEMONIC_CHECKSUM); - } - } - - public static List randomMnemonicCodes() { - return toMnemonicCodes(NumericUtil.generateRandomBytes(16)); - } - - public static String randomMnemonicStr() { - List mnemonicCodes=randomMnemonicCodes(); - return Joiner.on(" ").join(mnemonicCodes); - } - - - public static List toMnemonicCodes(byte[] entropy) { - try { - return MnemonicCode.INSTANCE.toMnemonic(entropy); - } catch (org.bitcoinj.crypto.MnemonicException.MnemonicLengthException e) { - throw new TokenException(Messages.MNEMONIC_INVALID_LENGTH); - } catch (Exception e) { - throw new TokenException(Messages.MNEMONIC_CHECKSUM); - } - } - -} +package org.consenlabs.tokencore.foundation.utils; + +import com.google.common.base.Joiner; +import org.bitcoinj.crypto.MnemonicCode; +import org.consenlabs.tokencore.wallet.model.Messages; +import org.consenlabs.tokencore.wallet.model.TokenException; + +import java.util.Arrays; +import java.util.List; + +public class MnemonicUtil { + public static void validateMnemonics(List mnemonicCodes) { + try { + MnemonicCode.INSTANCE.check(mnemonicCodes); + } catch (org.bitcoinj.crypto.MnemonicException.MnemonicLengthException e) { + throw new TokenException(Messages.MNEMONIC_INVALID_LENGTH); + } catch (org.bitcoinj.crypto.MnemonicException.MnemonicWordException e) { + throw new TokenException(Messages.MNEMONIC_BAD_WORD); + } catch (Exception e) { + throw new TokenException(Messages.MNEMONIC_CHECKSUM); + } + } + + public static List randomMnemonicCodes() { + return toMnemonicCodes(NumericUtil.generateRandomBytes(16)); + } + + public static String randomMnemonicStr() { + List mnemonicCodes=randomMnemonicCodes(); + return Joiner.on(" ").join(mnemonicCodes); + } + + + + + public static List toMnemonicCodes(String mnemonic) { + if (mnemonic == null) { + throw new TokenException(Messages.MNEMONIC_INVALID_LENGTH); + } + + String normalized = mnemonic.trim().replaceAll("\\s+", " "); + if (normalized.isEmpty()) { + throw new TokenException(Messages.MNEMONIC_INVALID_LENGTH); + } + + return Arrays.asList(normalized.split(" ")); + } + + public static List toMnemonicCodes(byte[] entropy) { + try { + return MnemonicCode.INSTANCE.toMnemonic(entropy); + } catch (org.bitcoinj.crypto.MnemonicException.MnemonicLengthException e) { + throw new TokenException(Messages.MNEMONIC_INVALID_LENGTH); + } catch (Exception e) { + throw new TokenException(Messages.MNEMONIC_CHECKSUM); + } + } + +} diff --git a/src/main/java/org/consenlabs/tokencore/wallet/Identity.java b/src/main/java/org/consenlabs/tokencore/wallet/Identity.java index 091207e..62f5905 100755 --- a/src/main/java/org/consenlabs/tokencore/wallet/Identity.java +++ b/src/main/java/org/consenlabs/tokencore/wallet/Identity.java @@ -97,7 +97,7 @@ public static Identity createIdentity(String name, String password, String passw public static Identity recoverIdentity(String mnemonic, String name, String password, String passwordHit, String network, String segWit) { - List mnemonicCodes = Arrays.asList(mnemonic.split(" ")); + List mnemonicCodes = MnemonicUtil.toMnemonicCodes(mnemonic); Metadata metadata = new Metadata(); metadata.setName(name); metadata.setPasswordHint(passwordHit); @@ -145,7 +145,7 @@ void removeWallet(String walletId) { public Wallet deriveWallet(String chainType, String password) { String mnemonic = exportIdentity(password); - List mnemonics = Arrays.asList(mnemonic.split(" ")); + List mnemonics = MnemonicUtil.toMnemonicCodes(mnemonic); return deriveWalletByMnemonics(chainType,password,mnemonics); } @@ -157,7 +157,7 @@ public Wallet deriveWalletByMnemonics(String chainType, String password, List deriveWallets(List chainTypes, String password) { String mnemonic = exportIdentity(password); - List mnemonics = Arrays.asList(mnemonic.split(" ")); + List mnemonics = MnemonicUtil.toMnemonicCodes(mnemonic); return deriveWalletsByMnemonics(chainTypes, password, mnemonics); } diff --git a/src/main/java/org/consenlabs/tokencore/wallet/WalletManager.java b/src/main/java/org/consenlabs/tokencore/wallet/WalletManager.java index f588ac4..7df8706 100755 --- a/src/main/java/org/consenlabs/tokencore/wallet/WalletManager.java +++ b/src/main/java/org/consenlabs/tokencore/wallet/WalletManager.java @@ -1,450 +1,478 @@ -package org.consenlabs.tokencore.wallet; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.MapperFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.base.Strings; -import com.google.common.io.CharSource; -import com.google.common.io.Files; -import org.bitcoinj.crypto.DeterministicKey; -import org.bitcoinj.wallet.DeterministicKeyChain; -import org.bitcoinj.wallet.DeterministicSeed; -import org.consenlabs.tokencore.foundation.utils.MnemonicUtil; -import org.consenlabs.tokencore.foundation.utils.NumericUtil; -import org.consenlabs.tokencore.wallet.address.AddressCreatorManager; -import org.consenlabs.tokencore.wallet.address.EthereumAddressCreator; -import org.consenlabs.tokencore.wallet.keystore.*; -import org.consenlabs.tokencore.wallet.model.*; -import org.consenlabs.tokencore.wallet.validators.PrivateKeyValidator; -import org.json.JSONObject; - -import javax.annotation.Nullable; -import java.io.File; -import java.io.IOException; -import java.nio.charset.Charset; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; - -public class WalletManager { - private static final ConcurrentHashMap keystoreMap = new ConcurrentHashMap<>(); - - private static final ObjectMapper WRITE_MAPPER = new ObjectMapper(); - private static final ObjectMapper READ_MAPPER = new ObjectMapper(); - - static { - WRITE_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL); - READ_MAPPER.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true); - READ_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - READ_MAPPER.configure(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES, true); - } - - public static KeystoreStorage storage; - - static ObjectMapper getWriteMapper() { - return WRITE_MAPPER; - } - - static Wallet createWallet(IMTKeystore keystore) { - File file = generateWalletFile(keystore.getId()); - writeToFile(keystore, file); - keystoreMap.put(keystore.getId(), keystore); - return new Wallet(keystore); - } - - public static ConcurrentHashMap getKeyMap(){ - return keystoreMap; - } - - public static void changePassword(String id, String oldPassword, String newPassword) { - IMTKeystore keystore = mustFindKeystoreById(id); - IMTKeystore newKeystore = (IMTKeystore) keystore.changePassword(oldPassword, newPassword); - flushWallet(newKeystore, true); - } - - public static String exportPrivateKey(String id, String password) { - Wallet wallet = mustFindWalletById(id); - return wallet.exportPrivateKey(password); - } - - public static List exportPrivateKeys(String id, String password) { - Wallet wallet = mustFindWalletById(id); - return wallet.exportPrivateKeys(password); - } - - public static MnemonicAndPath exportMnemonic(String id, String password) { - Wallet wallet = mustFindWalletById(id); - return wallet.exportMnemonic(password); - } - - public static String exportKeystore(String id, String password) { - Wallet wallet = mustFindWalletById(id); - return wallet.exportKeystore(password); - } - - public static void removeWallet(String id, String password) { - Wallet wallet = mustFindWalletById(id); - if (!wallet.verifyPassword(password)) { - throw new TokenException(Messages.WALLET_INVALID_PASSWORD); - } - if (wallet.delete(password)) { - Identity.getCurrentIdentity().removeWallet(id); - keystoreMap.remove(id); - } - } - - public static void clearKeystoreMap() { - keystoreMap.clear(); - } - - public static Wallet importWalletFromKeystore(Metadata metadata, String keystoreContent, String password, boolean overwrite) { - WalletKeystore importedKeystore = validateKeystore(keystoreContent, password); - - if (metadata.getSource() == null) - metadata.setSource(Metadata.FROM_KEYSTORE); - - String privateKey = NumericUtil.bytesToHex(importedKeystore.decryptCiphertext(password)); - try { - new PrivateKeyValidator(privateKey).validate(); - } catch (TokenException ex) { - if (Messages.PRIVATE_KEY_INVALID.equals(ex.getMessage())) { - throw new TokenException(Messages.KEYSTORE_CONTAINS_INVALID_PRIVATE_KEY); - } else { - throw ex; - } - } - return importWalletFromPrivateKey(metadata, privateKey, password, overwrite); - } - - public static Wallet importWalletFromPrivateKey(Metadata metadata, String prvKeyHex, String password, boolean overwrite) { - IMTKeystore keystore = V3Keystore.create(metadata, password, prvKeyHex); - Wallet wallet = flushWallet(keystore, overwrite); - Identity.getCurrentIdentity().addWallet(wallet); - return wallet; - } - - /** - * Just for import EOS wallet - */ - public static Wallet importWalletFromPrivateKeys(Metadata metadata, String accountName, List prvKeys, List permissions, String password, boolean overwrite) { - IMTKeystore keystore = null; - if (!ChainType.EOS.equalsIgnoreCase(metadata.getChainType())) { - throw new TokenException("This method is only for importing EOS wallet"); - } - keystore = EOSKeystore.create(metadata, password, accountName, prvKeys, permissions); - return persistWallet(keystore, overwrite); - } - - /** - * use the importWalletFromPrivateKeys - */ - @Deprecated - public static Wallet importWalletFromPrivateKey(Metadata metadata, String accountName, String prvKeyHex, String password, boolean overwrite) { - IMTKeystore keystore = LegacyEOSKeystore.create(metadata, accountName, password, prvKeyHex); - return persistWallet(keystore, overwrite); - } - - - /** - * import wallet from mnemonic - * - * @param metadata - * @param accountName only for EOS - * @param mnemonic - * @param path - * @param permissions only for EOS - * @param password - * @param overwrite - * @return - */ - public static Wallet importWalletFromMnemonic(Metadata metadata, @Nullable String accountName, String mnemonic, String path, @Nullable List permissions, String password, boolean overwrite) { - - if (metadata.getSource() == null) - metadata.setSource(Metadata.FROM_MNEMONIC); - IMTKeystore keystore = null; - List mnemonicCodes = Arrays.asList(mnemonic.split(" ")); - MnemonicUtil.validateMnemonics(mnemonicCodes); - switch (metadata.getChainType()) { - case ChainType.ETHEREUM: - case ChainType.TRON: - case ChainType.FILECOIN: - keystore = V3MnemonicKeystore.create(metadata, password, mnemonicCodes, path); - break; - case ChainType.BITCOIN: - case ChainType.LITECOIN: - case ChainType.DASH: - case ChainType.DOGECOIN: - case ChainType.BITCOINCASH: - case ChainType.BITCOINSV: - keystore = HDMnemonicKeystore.create(metadata, password, mnemonicCodes, path); - break; - case ChainType.EOS: - keystore = EOSKeystore.create(metadata, password, accountName, mnemonicCodes, path, permissions); - break; - default: - throw new TokenException(String.format("Mnemonic import not supported for chain: %s", metadata.getChainType())); - } - - return persistWallet(keystore, overwrite); - } - - public static Wallet importWalletFromMnemonic(Metadata metadata, String mnemonic, String path, String password, boolean overwrite) { - return importWalletFromMnemonic(metadata, null, mnemonic, path, null, password, overwrite); - } - - public static Wallet findWalletByPrivateKey(String chainType, String network, String privateKey, String segWit) { - if (ChainType.ETHEREUM.equals(chainType)) { - new PrivateKeyValidator(privateKey).validate(); - } - Network net = new Network(network); - String address = AddressCreatorManager.getInstance(chainType, net.isMainnet(), segWit).fromPrivateKey(privateKey); - return findWalletByAddress(chainType, address); - } - - public static Wallet findWalletByKeystore(String chainType, String keystoreContent, String password) { - WalletKeystore walletKeystore = validateKeystore(keystoreContent, password); - - byte[] prvKeyBytes = walletKeystore.decryptCiphertext(password); - String address = new EthereumAddressCreator().fromPrivateKey(prvKeyBytes); - return findWalletByAddress(chainType, address); - } - - public static Wallet findWalletByMnemonic(String chainType, String network, String mnemonic, String path, String segWit) { - List mnemonicCodes = Arrays.asList(mnemonic.split(" ")); - MnemonicUtil.validateMnemonics(mnemonicCodes); - DeterministicSeed seed = new DeterministicSeed(mnemonicCodes, null, "", 0L); - DeterministicKeyChain keyChain = DeterministicKeyChain.builder().seed(seed).build(); - if (Strings.isNullOrEmpty(path)) { - throw new TokenException(Messages.INVALID_MNEMONIC_PATH); - } - - if (ChainType.BITCOIN.equalsIgnoreCase(chainType)||ChainType.LITECOIN.equalsIgnoreCase(chainType)) { - path += "/0/0"; - } - - DeterministicKey key = keyChain.getKeyByPath(BIP44Util.generatePath(path), true); - Network net = new Network(network); - String address = AddressCreatorManager.getInstance(chainType, net.isMainnet(), segWit).fromPrivateKey(key.getPrivateKeyAsHex()); - return findWalletByAddress(chainType, address); - } - - public static Wallet switchBTCWalletMode(String id, String password, String model) { - Wallet wallet = mustFindWalletById(id); - // !!! Warning !!! You must verify password before you write content to keystore - if (!wallet.getMetadata().getChainType().equalsIgnoreCase(ChainType.BITCOIN)) - throw new TokenException("Ethereum wallet can't switch mode"); - Metadata metadata = wallet.getMetadata().clone(); - if (metadata.getSegWit().equalsIgnoreCase(model)) { - return wallet; - } - - metadata.setSegWit(model); - IMTKeystore keystore; - if (wallet.hasMnemonic()) { - - MnemonicAndPath mnemonicAndPath = wallet.exportMnemonic(password); - String path = BIP44Util.getBTCMnemonicPath(model, metadata.isMainNet()); - List mnemonicCodes = Arrays.asList(mnemonicAndPath.getMnemonic().split(" ")); - keystore = new HDMnemonicKeystore(metadata, password, mnemonicCodes, path, wallet.getId()); - } else { - String prvKey = wallet.exportPrivateKey(password); - keystore = new V3Keystore(metadata, password, prvKey, wallet.getId()); - - } - flushWallet(keystore, false); - keystoreMap.put(wallet.getId(), keystore); - return new Wallet(keystore); - } - - public static Wallet setAccountName(String id, String accountName) { - Wallet wallet = mustFindWalletById(id); - wallet.setAccountName(accountName); - return persistWallet(wallet.getKeystore(), true); - } - - static Wallet findWalletById(String id) { - IMTKeystore keystore = keystoreMap.get(id); - if (keystore != null) { - return new Wallet(keystore); - } else { - return null; - } - } - - public static Wallet mustFindWalletById(String id) { - IMTKeystore keystore = keystoreMap.get(id); - if (keystore == null) throw new TokenException(Messages.WALLET_NOT_FOUND); - return new Wallet(keystore); - } - - - static File generateWalletFile(String walletID) { - return new File(getDefaultKeyDirectory(), walletID + ".json"); - } - - - static File getDefaultKeyDirectory() { - File directory = new File(storage.getKeystoreDir(), "wallets"); - if (!directory.exists()) { - directory.mkdirs(); - } - return directory; - } - - static boolean cleanKeystoreDirectory() { - return deleteDir(getDefaultKeyDirectory()); - } - - private static Wallet persistWallet(IMTKeystore keystore, boolean overwrite) { - Wallet wallet = flushWallet(keystore, overwrite); - Identity.getCurrentIdentity().addWallet(wallet); - return wallet; - } - - private static IMTKeystore findKeystoreByAddress(String type, String address) { - if (Strings.isNullOrEmpty(address)) return null; - - for (IMTKeystore keystore : keystoreMap.values()) { - - if (Strings.isNullOrEmpty(keystore.getAddress())) { - continue; - } - - if (keystore.getMetadata().getChainType().equals(type) && keystore.getAddress().equals(address)) { - return keystore; - } - } - - return null; - } - - public static Wallet findWalletByAddress(String type, String address) { - IMTKeystore keystore = findKeystoreByAddress(type, address); - if (keystore != null) { - return new Wallet(keystore); - } - return null; - } - - - private static Wallet flushWallet(IMTKeystore keystore, boolean overwrite) { - - IMTKeystore existsKeystore = findKeystoreByAddress(keystore.getMetadata().getChainType(), keystore.getAddress()); - if (existsKeystore != null) { - if (!overwrite) { - throw new TokenException(Messages.WALLET_EXISTS); - } else { - keystore.setId(existsKeystore.getId()); - } - } - - File file = generateWalletFile(keystore.getId()); - writeToFile(keystore, file); - keystoreMap.put(keystore.getId(), keystore); - return new Wallet(keystore); - } - - private static void writeToFile(Keystore keyStore, File destination) { - try { - WRITE_MAPPER.writeValue(destination, keyStore); - } catch (IOException ex) { - throw new TokenException(Messages.WALLET_STORE_FAIL, ex); - } - } - - private static boolean deleteDir(File dir) { - if (dir.isDirectory()) { - String[] children = dir.list(); - if (children != null) { - for (String child : children) { - if (!deleteDir(new File(dir, child))) { - return false; - } - } - } - } - return dir.delete(); - } - - private static V3Keystore validateKeystore(String keystoreContent, String password) { - V3Keystore importedKeystore = unmarshalKeystore(keystoreContent, V3Keystore.class); - if (Strings.isNullOrEmpty(importedKeystore.getAddress()) || importedKeystore.getCrypto() == null) { - throw new TokenException(Messages.WALLET_INVALID_KEYSTORE); - } - - importedKeystore.getCrypto().validate(); - - if (!importedKeystore.verifyPassword(password)) - throw new TokenException(Messages.MAC_UNMATCH); - - byte[] prvKey = importedKeystore.decryptCiphertext(password); - String address = new EthereumAddressCreator().fromPrivateKey(prvKey); - if (Strings.isNullOrEmpty(address) || !address.equalsIgnoreCase(importedKeystore.getAddress())) { - throw new TokenException(Messages.PRIVATE_KEY_ADDRESS_NOT_MATCH); - } - return importedKeystore; - } - - private static IMTKeystore mustFindKeystoreById(String id) { - IMTKeystore keystore = keystoreMap.get(id); - if (keystore == null) { - throw new TokenException(Messages.WALLET_NOT_FOUND); - } - - return keystore; - } - - private static T unmarshalKeystore(String keystoreContent, Class clazz) { - try { - return READ_MAPPER.readValue(keystoreContent, clazz); - } catch (IOException ex) { - throw new TokenException(Messages.WALLET_INVALID_KEYSTORE, ex); - } - } - - public static void scanWallets() { - File directory = getDefaultKeyDirectory(); - - keystoreMap.clear(); - File[] files = directory.listFiles(); - if (files == null) { - return; - } - for (File file : files) { - if (!file.getName().startsWith("identity")) { - try { - IMTKeystore keystore = null; - CharSource charSource = Files.asCharSource(file, Charset.forName("UTF-8")); - String jsonContent = charSource.read(); - JSONObject jsonObject = new JSONObject(jsonContent); - int version = jsonObject.getInt("version"); - if (version == 3) { - if (jsonContent.contains("encMnemonic")) { - keystore = unmarshalKeystore(jsonContent, V3MnemonicKeystore.class); - } else if (jsonObject.has("imTokenMeta") && ChainType.EOS.equals(jsonObject.getJSONObject("imTokenMeta").getString("chainType"))) { - keystore = unmarshalKeystore(jsonContent, LegacyEOSKeystore.class); - } else { - keystore = unmarshalKeystore(jsonContent, V3Keystore.class); - } - } else if (version == 1) { - keystore = unmarshalKeystore(jsonContent, V3Keystore.class); - } else if (version == 44) { - keystore = unmarshalKeystore(jsonContent, HDMnemonicKeystore.class); - } else if (version == 10001) { - keystore = unmarshalKeystore(jsonContent, EOSKeystore.class); - } - - if (keystore != null) { - keystoreMap.put(keystore.getId(), keystore); - } - } catch (Exception ignored) { - } - } - } - } - - private WalletManager() { - } - -} +package org.consenlabs.tokencore.wallet; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Strings; +import com.google.common.io.CharSource; +import com.google.common.io.Files; +import org.bitcoinj.crypto.DeterministicKey; +import org.bitcoinj.wallet.DeterministicKeyChain; +import org.bitcoinj.wallet.DeterministicSeed; +import org.consenlabs.tokencore.foundation.utils.MnemonicUtil; +import org.consenlabs.tokencore.foundation.utils.NumericUtil; +import org.consenlabs.tokencore.wallet.address.AddressCreatorManager; +import org.consenlabs.tokencore.wallet.address.EthereumAddressCreator; +import org.consenlabs.tokencore.wallet.keystore.*; +import org.consenlabs.tokencore.wallet.model.*; +import org.consenlabs.tokencore.wallet.validators.PrivateKeyValidator; +import org.json.JSONObject; + +import javax.annotation.Nullable; +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +public class WalletManager { + private static final ConcurrentHashMap keystoreMap = new ConcurrentHashMap<>(); + + private static final ObjectMapper WRITE_MAPPER = new ObjectMapper(); + private static final ObjectMapper READ_MAPPER = new ObjectMapper(); + + static { + WRITE_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL); + READ_MAPPER.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true); + READ_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + READ_MAPPER.configure(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES, true); + } + + public static KeystoreStorage storage; + + static ObjectMapper getWriteMapper() { + return WRITE_MAPPER; + } + + static Wallet createWallet(IMTKeystore keystore) { + File file = generateWalletFile(keystore.getId()); + writeToFile(keystore, file); + keystoreMap.put(keystore.getId(), keystore); + return new Wallet(keystore); + } + + public static ConcurrentHashMap getKeyMap(){ + return keystoreMap; + } + + public static void changePassword(String id, String oldPassword, String newPassword) { + IMTKeystore keystore = mustFindKeystoreById(id); + IMTKeystore newKeystore = (IMTKeystore) keystore.changePassword(oldPassword, newPassword); + flushWallet(newKeystore, true); + } + + public static String exportPrivateKey(String id, String password) { + Wallet wallet = mustFindWalletById(id); + return wallet.exportPrivateKey(password); + } + + public static List exportPrivateKeys(String id, String password) { + Wallet wallet = mustFindWalletById(id); + return wallet.exportPrivateKeys(password); + } + + public static MnemonicAndPath exportMnemonic(String id, String password) { + Wallet wallet = mustFindWalletById(id); + return wallet.exportMnemonic(password); + } + + public static String exportKeystore(String id, String password) { + Wallet wallet = mustFindWalletById(id); + return wallet.exportKeystore(password); + } + + public static void removeWallet(String id, String password) { + Wallet wallet = mustFindWalletById(id); + if (!wallet.verifyPassword(password)) { + throw new TokenException(Messages.WALLET_INVALID_PASSWORD); + } + if (wallet.delete(password)) { + Identity.getCurrentIdentity().removeWallet(id); + keystoreMap.remove(id); + } + } + + public static void clearKeystoreMap() { + keystoreMap.clear(); + } + + public static Wallet importWalletFromKeystore(Metadata metadata, String keystoreContent, String password, boolean overwrite) { + WalletKeystore importedKeystore = validateKeystore(keystoreContent, password, metadata.getChainType()); + + if (metadata.getSource() == null) + metadata.setSource(Metadata.FROM_KEYSTORE); + + String privateKey = NumericUtil.bytesToHex(importedKeystore.decryptCiphertext(password)); + try { + new PrivateKeyValidator(privateKey).validate(); + } catch (TokenException ex) { + if (Messages.PRIVATE_KEY_INVALID.equals(ex.getMessage())) { + throw new TokenException(Messages.KEYSTORE_CONTAINS_INVALID_PRIVATE_KEY); + } else { + throw ex; + } + } + return importWalletFromPrivateKey(metadata, privateKey, password, overwrite); + } + + public static Wallet importWalletFromPrivateKey(Metadata metadata, String prvKeyHex, String password, boolean overwrite) { + IMTKeystore keystore = V3Keystore.create(metadata, password, prvKeyHex); + Wallet wallet = flushWallet(keystore, overwrite); + Identity.getCurrentIdentity().addWallet(wallet); + return wallet; + } + + /** + * Just for import EOS wallet + */ + public static Wallet importWalletFromPrivateKeys(Metadata metadata, String accountName, List prvKeys, List permissions, String password, boolean overwrite) { + IMTKeystore keystore = null; + if (!ChainType.EOS.equalsIgnoreCase(metadata.getChainType())) { + throw new TokenException("This method is only for importing EOS wallet"); + } + keystore = EOSKeystore.create(metadata, password, accountName, prvKeys, permissions); + return persistWallet(keystore, overwrite); + } + + /** + * use the importWalletFromPrivateKeys + */ + @Deprecated + public static Wallet importWalletFromPrivateKey(Metadata metadata, String accountName, String prvKeyHex, String password, boolean overwrite) { + IMTKeystore keystore = LegacyEOSKeystore.create(metadata, accountName, password, prvKeyHex); + return persistWallet(keystore, overwrite); + } + + + /** + * import wallet from mnemonic + * + * @param metadata + * @param accountName only for EOS + * @param mnemonic + * @param path + * @param permissions only for EOS + * @param password + * @param overwrite + * @return + */ + public static Wallet importWalletFromMnemonic(Metadata metadata, @Nullable String accountName, String mnemonic, String path, @Nullable List permissions, String password, boolean overwrite) { + + if (metadata.getSource() == null) + metadata.setSource(Metadata.FROM_MNEMONIC); + IMTKeystore keystore = null; + List mnemonicCodes = MnemonicUtil.toMnemonicCodes(mnemonic); + MnemonicUtil.validateMnemonics(mnemonicCodes); + switch (metadata.getChainType()) { + case ChainType.ETHEREUM: + case ChainType.TRON: + case ChainType.FILECOIN: + keystore = V3MnemonicKeystore.create(metadata, password, mnemonicCodes, path); + break; + case ChainType.BITCOIN: + case ChainType.LITECOIN: + case ChainType.DASH: + case ChainType.DOGECOIN: + case ChainType.BITCOINCASH: + case ChainType.BITCOINSV: + keystore = HDMnemonicKeystore.create(metadata, password, mnemonicCodes, path); + break; + case ChainType.EOS: + keystore = EOSKeystore.create(metadata, password, accountName, mnemonicCodes, path, permissions); + break; + default: + throw new TokenException(String.format("Mnemonic import not supported for chain: %s", metadata.getChainType())); + } + + return persistWallet(keystore, overwrite); + } + + public static Wallet importWalletFromMnemonic(Metadata metadata, String mnemonic, String path, String password, boolean overwrite) { + return importWalletFromMnemonic(metadata, null, mnemonic, path, null, password, overwrite); + } + + public static Wallet findWalletByPrivateKey(String chainType, String network, String privateKey, String segWit) { + if (ChainType.ETHEREUM.equals(chainType)) { + new PrivateKeyValidator(privateKey).validate(); + } + Network net = new Network(network); + String address = AddressCreatorManager.getInstance(chainType, net.isMainnet(), segWit).fromPrivateKey(privateKey); + return findWalletByAddress(chainType, address); + } + + public static Wallet findWalletByKeystore(String chainType, String keystoreContent, String password) { + WalletKeystore walletKeystore = validateKeystore(keystoreContent, password, chainType); + + byte[] prvKeyBytes = walletKeystore.decryptCiphertext(password); + Metadata metadata = ((IMTKeystore) walletKeystore).getMetadata(); + String address = deriveAddressByChain(chainType, metadata, prvKeyBytes); + return findWalletByAddress(chainType, address); + } + + public static Wallet findWalletByMnemonic(String chainType, String network, String mnemonic, String path, String segWit) { + List mnemonicCodes = MnemonicUtil.toMnemonicCodes(mnemonic); + MnemonicUtil.validateMnemonics(mnemonicCodes); + DeterministicSeed seed = new DeterministicSeed(mnemonicCodes, null, "", 0L); + DeterministicKeyChain keyChain = DeterministicKeyChain.builder().seed(seed).build(); + if (Strings.isNullOrEmpty(path)) { + throw new TokenException(Messages.INVALID_MNEMONIC_PATH); + } + + if (isBitcoinFamily(chainType)) { + path += "/0/0"; + } + + DeterministicKey key = keyChain.getKeyByPath(BIP44Util.generatePath(path), true); + Network net = new Network(network); + String address = AddressCreatorManager.getInstance(chainType, net.isMainnet(), segWit).fromPrivateKey(key.getPrivateKeyAsHex()); + return findWalletByAddress(chainType, address); + } + + public static Wallet switchBTCWalletMode(String id, String password, String model) { + Wallet wallet = mustFindWalletById(id); + // !!! Warning !!! You must verify password before you write content to keystore + if (!wallet.getMetadata().getChainType().equalsIgnoreCase(ChainType.BITCOIN)) + throw new TokenException("Only Bitcoin wallets can switch SegWit mode"); + Metadata metadata = wallet.getMetadata().clone(); + if (metadata.getSegWit().equalsIgnoreCase(model)) { + return wallet; + } + + metadata.setSegWit(model); + IMTKeystore keystore; + if (wallet.hasMnemonic()) { + + MnemonicAndPath mnemonicAndPath = wallet.exportMnemonic(password); + String path = BIP44Util.getBTCMnemonicPath(model, metadata.isMainNet()); + List mnemonicCodes = MnemonicUtil.toMnemonicCodes(mnemonicAndPath.getMnemonic()); + keystore = new HDMnemonicKeystore(metadata, password, mnemonicCodes, path, wallet.getId()); + } else { + String prvKey = wallet.exportPrivateKey(password); + keystore = new V3Keystore(metadata, password, prvKey, wallet.getId()); + + } + flushWallet(keystore, false); + keystoreMap.put(wallet.getId(), keystore); + return new Wallet(keystore); + } + + public static Wallet setAccountName(String id, String accountName) { + Wallet wallet = mustFindWalletById(id); + wallet.setAccountName(accountName); + return persistWallet(wallet.getKeystore(), true); + } + + static Wallet findWalletById(String id) { + IMTKeystore keystore = keystoreMap.get(id); + if (keystore != null) { + return new Wallet(keystore); + } else { + return null; + } + } + + public static Wallet mustFindWalletById(String id) { + IMTKeystore keystore = keystoreMap.get(id); + if (keystore == null) throw new TokenException(Messages.WALLET_NOT_FOUND); + return new Wallet(keystore); + } + + + static File generateWalletFile(String walletID) { + return new File(getDefaultKeyDirectory(), walletID + ".json"); + } + + + static File getDefaultKeyDirectory() { + File directory = new File(storage.getKeystoreDir(), "wallets"); + if (!directory.exists()) { + directory.mkdirs(); + } + return directory; + } + + static boolean cleanKeystoreDirectory() { + return deleteDir(getDefaultKeyDirectory()); + } + + private static Wallet persistWallet(IMTKeystore keystore, boolean overwrite) { + Wallet wallet = flushWallet(keystore, overwrite); + Identity.getCurrentIdentity().addWallet(wallet); + return wallet; + } + + private static IMTKeystore findKeystoreByAddress(String type, String address) { + if (Strings.isNullOrEmpty(address)) return null; + + for (IMTKeystore keystore : keystoreMap.values()) { + + if (Strings.isNullOrEmpty(keystore.getAddress())) { + continue; + } + + if (keystore.getMetadata().getChainType().equals(type) && keystore.getAddress().equals(address)) { + return keystore; + } + } + + return null; + } + + public static Wallet findWalletByAddress(String type, String address) { + IMTKeystore keystore = findKeystoreByAddress(type, address); + if (keystore != null) { + return new Wallet(keystore); + } + return null; + } + + + private static Wallet flushWallet(IMTKeystore keystore, boolean overwrite) { + + IMTKeystore existsKeystore = findKeystoreByAddress(keystore.getMetadata().getChainType(), keystore.getAddress()); + if (existsKeystore != null) { + if (!overwrite) { + throw new TokenException(Messages.WALLET_EXISTS); + } else { + keystore.setId(existsKeystore.getId()); + } + } + + File file = generateWalletFile(keystore.getId()); + writeToFile(keystore, file); + keystoreMap.put(keystore.getId(), keystore); + return new Wallet(keystore); + } + + private static void writeToFile(Keystore keyStore, File destination) { + try { + WRITE_MAPPER.writeValue(destination, keyStore); + } catch (IOException ex) { + throw new TokenException(Messages.WALLET_STORE_FAIL, ex); + } + } + + private static boolean deleteDir(File dir) { + if (dir.isDirectory()) { + String[] children = dir.list(); + if (children != null) { + for (String child : children) { + if (!deleteDir(new File(dir, child))) { + return false; + } + } + } + } + return dir.delete(); + } + + private static V3Keystore validateKeystore(String keystoreContent, String password, String chainType) { + V3Keystore importedKeystore = unmarshalKeystore(keystoreContent, V3Keystore.class); + if (Strings.isNullOrEmpty(importedKeystore.getAddress()) || importedKeystore.getCrypto() == null) { + throw new TokenException(Messages.WALLET_INVALID_KEYSTORE); + } + + importedKeystore.getCrypto().validate(); + + if (!importedKeystore.verifyPassword(password)) + throw new TokenException(Messages.MAC_UNMATCH); + + byte[] prvKey = importedKeystore.decryptCiphertext(password); + Metadata metadata = importedKeystore.getMetadata(); + String derivedAddress = deriveAddressByChain(chainType, metadata, prvKey); + if (isEvmFamily(chainType)) { + if (Strings.isNullOrEmpty(derivedAddress) || !derivedAddress.equalsIgnoreCase(importedKeystore.getAddress())) { + throw new TokenException(Messages.PRIVATE_KEY_ADDRESS_NOT_MATCH); + } + } + return importedKeystore; + } + + private static String deriveAddressByChain(String chainType, Metadata metadata, byte[] prvKey) { + boolean isMainnet = metadata != null && metadata.isMainNet(); + String segWit = metadata == null ? Metadata.NONE : metadata.getSegWit(); + + if (isEvmFamily(chainType)) { + return new EthereumAddressCreator().fromPrivateKey(prvKey); + } + + return AddressCreatorManager.getInstance(chainType, isMainnet, segWit).fromPrivateKey(prvKey); + } + + + private static boolean isBitcoinFamily(String chainType) { + return ChainType.BITCOIN.equalsIgnoreCase(chainType) + || ChainType.LITECOIN.equalsIgnoreCase(chainType) + || ChainType.DOGECOIN.equalsIgnoreCase(chainType) + || ChainType.DASH.equalsIgnoreCase(chainType) + || ChainType.BITCOINCASH.equalsIgnoreCase(chainType) + || ChainType.BITCOINSV.equalsIgnoreCase(chainType); + } + private static boolean isEvmFamily(String chainType) { + return ChainType.ETHEREUM.equalsIgnoreCase(chainType) || chainType.toUpperCase().contains("EVM"); + } + + private static IMTKeystore mustFindKeystoreById(String id) { + IMTKeystore keystore = keystoreMap.get(id); + if (keystore == null) { + throw new TokenException(Messages.WALLET_NOT_FOUND); + } + + return keystore; + } + + private static T unmarshalKeystore(String keystoreContent, Class clazz) { + try { + return READ_MAPPER.readValue(keystoreContent, clazz); + } catch (IOException ex) { + throw new TokenException(Messages.WALLET_INVALID_KEYSTORE, ex); + } + } + + public static void scanWallets() { + File directory = getDefaultKeyDirectory(); + + keystoreMap.clear(); + File[] files = directory.listFiles(); + if (files == null) { + return; + } + for (File file : files) { + if (!file.getName().startsWith("identity")) { + try { + IMTKeystore keystore = null; + CharSource charSource = Files.asCharSource(file, Charset.forName("UTF-8")); + String jsonContent = charSource.read(); + JSONObject jsonObject = new JSONObject(jsonContent); + int version = jsonObject.getInt("version"); + if (version == 3) { + if (jsonContent.contains("encMnemonic")) { + keystore = unmarshalKeystore(jsonContent, V3MnemonicKeystore.class); + } else if (jsonObject.has("imTokenMeta") && ChainType.EOS.equals(jsonObject.getJSONObject("imTokenMeta").getString("chainType"))) { + keystore = unmarshalKeystore(jsonContent, LegacyEOSKeystore.class); + } else { + keystore = unmarshalKeystore(jsonContent, V3Keystore.class); + } + } else if (version == 1) { + keystore = unmarshalKeystore(jsonContent, V3Keystore.class); + } else if (version == 44) { + keystore = unmarshalKeystore(jsonContent, HDMnemonicKeystore.class); + } else if (version == 10001) { + keystore = unmarshalKeystore(jsonContent, EOSKeystore.class); + } + + if (keystore != null) { + keystoreMap.put(keystore.getId(), keystore); + } + } catch (Exception ignored) { + } + } + } + } + + private WalletManager() { + } + +} diff --git a/src/main/java/org/consenlabs/tokencore/wallet/keystore/HDMnemonicKeystore.java b/src/main/java/org/consenlabs/tokencore/wallet/keystore/HDMnemonicKeystore.java index cc95b44..6dda4af 100755 --- a/src/main/java/org/consenlabs/tokencore/wallet/keystore/HDMnemonicKeystore.java +++ b/src/main/java/org/consenlabs/tokencore/wallet/keystore/HDMnemonicKeystore.java @@ -1,191 +1,191 @@ -package org.consenlabs.tokencore.wallet.keystore; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.google.common.base.Joiner; -import com.google.common.base.Strings; -import com.google.common.io.BaseEncoding; -import com.subgraph.orchid.encoders.Hex; -import org.bitcoinj.core.NetworkParameters; -import org.bitcoinj.crypto.ChildNumber; -import org.bitcoinj.crypto.DeterministicKey; -import org.bitcoinj.crypto.HDKeyDerivation; -import org.bitcoinj.wallet.DeterministicKeyChain; -import org.bitcoinj.wallet.DeterministicSeed; -import org.consenlabs.tokencore.foundation.crypto.AES; -import org.consenlabs.tokencore.foundation.crypto.Crypto; -import org.consenlabs.tokencore.foundation.crypto.EncPair; -import org.consenlabs.tokencore.foundation.utils.DateUtil; -import org.consenlabs.tokencore.foundation.utils.MetaUtil; -import org.consenlabs.tokencore.foundation.utils.MnemonicUtil; -import org.consenlabs.tokencore.wallet.address.SegWitBitcoinAddressCreator; -import org.consenlabs.tokencore.wallet.model.BIP44Util; -import org.consenlabs.tokencore.wallet.model.Messages; -import org.consenlabs.tokencore.wallet.model.Metadata; -import org.consenlabs.tokencore.wallet.model.TokenException; - -import java.nio.charset.Charset; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; - -/** - * Created by xyz on 2018/2/5. - */ - -public final class HDMnemonicKeystore extends IMTKeystore implements EncMnemonicKeystore { - - // !!! Don't use this key in production !!! - public static String XPubCommonKey128 = "B888D25EC8C12BD5043777B1AC49F872"; - public static String XPubCommonIv = "9C0C30889CBCC5E01AB5B2BB88715799"; - - static int VERSION = 44; - private EncPair encMnemonic; - private String mnemonicPath; - private String xpub; - - public Info getInfo() { - return info; - } - - public void setInfo(Info info) { - this.info = info; - } - - private Info info; - - @Override - public EncPair getEncMnemonic() { - return encMnemonic; - } - - @Override - public void setEncMnemonic(EncPair encMnemonic) { - this.encMnemonic = encMnemonic; - } - - @Override - public String getMnemonicPath() { - return mnemonicPath; - } - - public void setMnemonicPath(String mnemonicPath) { - this.mnemonicPath = mnemonicPath; - } - - public String getXpub() { - return this.xpub; - } - - public void setXpub(String xpub) { - this.xpub = xpub; - } - - public HDMnemonicKeystore() { - super(); - } - - public static HDMnemonicKeystore create(Metadata metadata, String password, List mnemonics, String path) { - return new HDMnemonicKeystore(metadata, password, mnemonics, path, ""); - } - - public HDMnemonicKeystore(Metadata metadata, String password, List mnemonics, String path, String id) { - MnemonicUtil.validateMnemonics(mnemonics); - DeterministicSeed seed = new DeterministicSeed(mnemonics, null, "", 0L); - DeterministicKeyChain keyChain = DeterministicKeyChain.builder().seed(seed).build(); - this.mnemonicPath = path; - - DeterministicKey parent = keyChain.getKeyByPath(BIP44Util.generatePath(path), true); - NetworkParameters networkParameters = MetaUtil.getNetWork(metadata); - this.xpub = parent.serializePubB58(networkParameters); - String xprv = parent.serializePrivB58(networkParameters); - DeterministicKey mainAddressKey = keyChain.getKeyByPath(BIP44Util.generatePath(path + "/0/0"), true); - if (Metadata.P2WPKH.equals(metadata.getSegWit())) { - this.address = new SegWitBitcoinAddressCreator(networkParameters).fromPrivateKey(mainAddressKey.getPrivateKeyAsHex()); - } else { - this.address = mainAddressKey.toAddress(networkParameters).toBase58(); - } - if (metadata.getTimestamp() == 0) { - metadata.setTimestamp(DateUtil.getUTCTime()); - } - metadata.setWalletType(Metadata.HD); - - this.crypto = Crypto.createPBKDF2CryptoWithKDFCached(password, xprv.getBytes(Charset.forName("UTF-8"))); - this.metadata = metadata; - this.encMnemonic = crypto.deriveEncPair(password, Joiner.on(" ").join(mnemonics).getBytes()); - this.crypto.clearCachedDerivedKey(); - - this.version = VERSION; - this.info = new Info(); - this.id = Strings.isNullOrEmpty(id) ? UUID.randomUUID().toString() : id; - } - - - @Override - public Keystore changePassword(String oldPassword, String newPassword) { - String mnemonic = new String(getCrypto().decryptEncPair(oldPassword, encMnemonic)); - List mnemonicCodes = Arrays.asList(mnemonic.split(" ")); - return new HDMnemonicKeystore(metadata, newPassword, mnemonicCodes, this.mnemonicPath, this.id); - } - - @JsonIgnore - public String getEncryptXPub() { - String plainText = this.xpub; - try { - - byte[] commonKey128 = Hex.decode(XPubCommonKey128); - byte[] clean = plainText.getBytes(); - byte[] commonIv = Hex.decode(XPubCommonIv); - byte[] encrypted = AES.encryptByCBC(clean, commonKey128, commonIv); - return BaseEncoding.base64().encode(encrypted); - } catch (Exception ex) { - throw new TokenException(Messages.ENCRYPT_XPUB_ERROR); - } - } - - public String newReceiveAddress(int nextIdx) { - NetworkParameters networkParameters = MetaUtil.getNetWork(this.metadata); - DeterministicKey key = DeterministicKey.deserializeB58(this.xpub, networkParameters); - DeterministicKey changeKey = HDKeyDerivation.deriveChildKey(key, ChildNumber.ZERO); - DeterministicKey indexKey = HDKeyDerivation.deriveChildKey(changeKey, new ChildNumber(nextIdx)); - if (Metadata.P2WPKH.equals(metadata.getSegWit())) { - return new SegWitBitcoinAddressCreator(networkParameters).fromPrivateKey(indexKey).toBase58(); - } else { - return indexKey.toAddress(networkParameters).toBase58(); - } - } - - - public static class Info { - private String curve = "secp256k1"; - private String purpose = "sign"; - - public Info() { - } - - public String getCurve() { - return curve; - } - - public void setCurve(String curve) { - this.curve = curve; - } - - public String getPurpose() { - return purpose; - } - - public void setPurpose(String purpose) { - this.purpose = purpose; - } - - @Deprecated - public String getPurpuse() { - return purpose; - } - - @Deprecated - public void setPurpuse(String purpuse) { - this.purpose = purpuse; - } - } -} +package org.consenlabs.tokencore.wallet.keystore; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.google.common.base.Joiner; +import com.google.common.base.Strings; +import com.google.common.io.BaseEncoding; +import com.subgraph.orchid.encoders.Hex; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.crypto.ChildNumber; +import org.bitcoinj.crypto.DeterministicKey; +import org.bitcoinj.crypto.HDKeyDerivation; +import org.bitcoinj.wallet.DeterministicKeyChain; +import org.bitcoinj.wallet.DeterministicSeed; +import org.consenlabs.tokencore.foundation.crypto.AES; +import org.consenlabs.tokencore.foundation.crypto.Crypto; +import org.consenlabs.tokencore.foundation.crypto.EncPair; +import org.consenlabs.tokencore.foundation.utils.DateUtil; +import org.consenlabs.tokencore.foundation.utils.MetaUtil; +import org.consenlabs.tokencore.foundation.utils.MnemonicUtil; +import org.consenlabs.tokencore.wallet.address.SegWitBitcoinAddressCreator; +import org.consenlabs.tokencore.wallet.model.BIP44Util; +import org.consenlabs.tokencore.wallet.model.Messages; +import org.consenlabs.tokencore.wallet.model.Metadata; +import org.consenlabs.tokencore.wallet.model.TokenException; + +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +/** + * Created by xyz on 2018/2/5. + */ + +public final class HDMnemonicKeystore extends IMTKeystore implements EncMnemonicKeystore { + + // !!! Don't use this key in production !!! + public static String XPubCommonKey128 = "B888D25EC8C12BD5043777B1AC49F872"; + public static String XPubCommonIv = "9C0C30889CBCC5E01AB5B2BB88715799"; + + static int VERSION = 44; + private EncPair encMnemonic; + private String mnemonicPath; + private String xpub; + + public Info getInfo() { + return info; + } + + public void setInfo(Info info) { + this.info = info; + } + + private Info info; + + @Override + public EncPair getEncMnemonic() { + return encMnemonic; + } + + @Override + public void setEncMnemonic(EncPair encMnemonic) { + this.encMnemonic = encMnemonic; + } + + @Override + public String getMnemonicPath() { + return mnemonicPath; + } + + public void setMnemonicPath(String mnemonicPath) { + this.mnemonicPath = mnemonicPath; + } + + public String getXpub() { + return this.xpub; + } + + public void setXpub(String xpub) { + this.xpub = xpub; + } + + public HDMnemonicKeystore() { + super(); + } + + public static HDMnemonicKeystore create(Metadata metadata, String password, List mnemonics, String path) { + return new HDMnemonicKeystore(metadata, password, mnemonics, path, ""); + } + + public HDMnemonicKeystore(Metadata metadata, String password, List mnemonics, String path, String id) { + MnemonicUtil.validateMnemonics(mnemonics); + DeterministicSeed seed = new DeterministicSeed(mnemonics, null, "", 0L); + DeterministicKeyChain keyChain = DeterministicKeyChain.builder().seed(seed).build(); + this.mnemonicPath = path; + + DeterministicKey parent = keyChain.getKeyByPath(BIP44Util.generatePath(path), true); + NetworkParameters networkParameters = MetaUtil.getNetWork(metadata); + this.xpub = parent.serializePubB58(networkParameters); + String xprv = parent.serializePrivB58(networkParameters); + DeterministicKey mainAddressKey = keyChain.getKeyByPath(BIP44Util.generatePath(path + "/0/0"), true); + if (Metadata.P2WPKH.equals(metadata.getSegWit())) { + this.address = new SegWitBitcoinAddressCreator(networkParameters).fromPrivateKey(mainAddressKey.getPrivateKeyAsHex()); + } else { + this.address = mainAddressKey.toAddress(networkParameters).toBase58(); + } + if (metadata.getTimestamp() == 0) { + metadata.setTimestamp(DateUtil.getUTCTime()); + } + metadata.setWalletType(Metadata.HD); + + this.crypto = Crypto.createPBKDF2CryptoWithKDFCached(password, xprv.getBytes(Charset.forName("UTF-8"))); + this.metadata = metadata; + this.encMnemonic = crypto.deriveEncPair(password, Joiner.on(" ").join(mnemonics).getBytes()); + this.crypto.clearCachedDerivedKey(); + + this.version = VERSION; + this.info = new Info(); + this.id = Strings.isNullOrEmpty(id) ? UUID.randomUUID().toString() : id; + } + + + @Override + public Keystore changePassword(String oldPassword, String newPassword) { + String mnemonic = new String(getCrypto().decryptEncPair(oldPassword, encMnemonic)); + List mnemonicCodes = MnemonicUtil.toMnemonicCodes(mnemonic); + return new HDMnemonicKeystore(metadata, newPassword, mnemonicCodes, this.mnemonicPath, this.id); + } + + @JsonIgnore + public String getEncryptXPub() { + String plainText = this.xpub; + try { + + byte[] commonKey128 = Hex.decode(XPubCommonKey128); + byte[] clean = plainText.getBytes(); + byte[] commonIv = Hex.decode(XPubCommonIv); + byte[] encrypted = AES.encryptByCBC(clean, commonKey128, commonIv); + return BaseEncoding.base64().encode(encrypted); + } catch (Exception ex) { + throw new TokenException(Messages.ENCRYPT_XPUB_ERROR); + } + } + + public String newReceiveAddress(int nextIdx) { + NetworkParameters networkParameters = MetaUtil.getNetWork(this.metadata); + DeterministicKey key = DeterministicKey.deserializeB58(this.xpub, networkParameters); + DeterministicKey changeKey = HDKeyDerivation.deriveChildKey(key, ChildNumber.ZERO); + DeterministicKey indexKey = HDKeyDerivation.deriveChildKey(changeKey, new ChildNumber(nextIdx)); + if (Metadata.P2WPKH.equals(metadata.getSegWit())) { + return new SegWitBitcoinAddressCreator(networkParameters).fromPrivateKey(indexKey).toBase58(); + } else { + return indexKey.toAddress(networkParameters).toBase58(); + } + } + + + public static class Info { + private String curve = "secp256k1"; + private String purpose = "sign"; + + public Info() { + } + + public String getCurve() { + return curve; + } + + public void setCurve(String curve) { + this.curve = curve; + } + + public String getPurpose() { + return purpose; + } + + public void setPurpose(String purpose) { + this.purpose = purpose; + } + + @Deprecated + public String getPurpuse() { + return purpose; + } + + @Deprecated + public void setPurpuse(String purpuse) { + this.purpose = purpuse; + } + } +} diff --git a/src/main/java/org/consenlabs/tokencore/wallet/keystore/V3MnemonicKeystore.java b/src/main/java/org/consenlabs/tokencore/wallet/keystore/V3MnemonicKeystore.java index 0c2db31..a3aaa41 100755 --- a/src/main/java/org/consenlabs/tokencore/wallet/keystore/V3MnemonicKeystore.java +++ b/src/main/java/org/consenlabs/tokencore/wallet/keystore/V3MnemonicKeystore.java @@ -1,86 +1,86 @@ -package org.consenlabs.tokencore.wallet.keystore; - -import com.google.common.base.Joiner; -import com.google.common.base.Strings; -import org.bitcoinj.crypto.ChildNumber; -import org.bitcoinj.wallet.DeterministicKeyChain; -import org.bitcoinj.wallet.DeterministicSeed; -import org.consenlabs.tokencore.foundation.crypto.Crypto; -import org.consenlabs.tokencore.foundation.crypto.EncPair; -import org.consenlabs.tokencore.foundation.utils.DateUtil; -import org.consenlabs.tokencore.foundation.utils.MnemonicUtil; -import org.consenlabs.tokencore.wallet.address.AddressCreatorManager; -import org.consenlabs.tokencore.wallet.model.BIP44Util; -import org.consenlabs.tokencore.wallet.model.Metadata; - -import java.util.Arrays; -import java.util.List; -import java.util.UUID; - -/** - * Created by xyz on 2018/2/5. - */ - -public class V3MnemonicKeystore extends IMTKeystore implements EncMnemonicKeystore, ExportableKeystore { - private static final int VERSION = 3; - private EncPair encMnemonic; - private String mnemonicPath; - - @Override - public EncPair getEncMnemonic() { - return encMnemonic; - } - - @Override - public void setEncMnemonic(EncPair encMnemonic) { - this.encMnemonic = encMnemonic; - } - - @Override - public String getMnemonicPath() { - return this.mnemonicPath; - } - - public void setMnemonicPath(String mnemonicPath) { - this.mnemonicPath = mnemonicPath; - } - - public V3MnemonicKeystore() { - super(); - } - - public static V3MnemonicKeystore create(Metadata metadata, String password, List mnemonicCodes, String path) { - return new V3MnemonicKeystore(metadata, password, mnemonicCodes, path, ""); - } - - - private V3MnemonicKeystore(Metadata metadata, String password, List mnemonicCodes, String path, String id) { - MnemonicUtil.validateMnemonics(mnemonicCodes); - DeterministicSeed seed = new DeterministicSeed(mnemonicCodes, null, "", 0L); - DeterministicKeyChain keyChain = DeterministicKeyChain.builder().seed(seed).build(); - - this.mnemonicPath = path; - List zeroPath = BIP44Util.generatePath(path); - - byte[] prvKeyBytes = keyChain.getKeyByPath(zeroPath, true).getPrivKeyBytes(); - this.crypto = Crypto.createPBKDF2CryptoWithKDFCached(password, prvKeyBytes); - this.encMnemonic = crypto.deriveEncPair(password, Joiner.on(" ").join(mnemonicCodes).getBytes()); - this.crypto.clearCachedDerivedKey(); - - this.address = AddressCreatorManager.getInstance(metadata.getChainType(), metadata.isMainNet(), metadata.getSegWit()).fromPrivateKey(prvKeyBytes); - metadata.setTimestamp(DateUtil.getUTCTime()); - metadata.setWalletType(Metadata.V3); - this.metadata = metadata; - this.version = VERSION; - this.id = Strings.isNullOrEmpty(id) ? UUID.randomUUID().toString() : id; - } - - - - @Override - public Keystore changePassword(String oldPassword, String newPassword) { - String mnemonic = new String(getCrypto().decryptEncPair(oldPassword, this.getEncMnemonic())); - List mnemonicCodes = Arrays.asList(mnemonic.split(" ")); - return new V3MnemonicKeystore(this.metadata, newPassword, mnemonicCodes, this.mnemonicPath, this.id); - } -} +package org.consenlabs.tokencore.wallet.keystore; + +import com.google.common.base.Joiner; +import com.google.common.base.Strings; +import org.bitcoinj.crypto.ChildNumber; +import org.bitcoinj.wallet.DeterministicKeyChain; +import org.bitcoinj.wallet.DeterministicSeed; +import org.consenlabs.tokencore.foundation.crypto.Crypto; +import org.consenlabs.tokencore.foundation.crypto.EncPair; +import org.consenlabs.tokencore.foundation.utils.DateUtil; +import org.consenlabs.tokencore.foundation.utils.MnemonicUtil; +import org.consenlabs.tokencore.wallet.address.AddressCreatorManager; +import org.consenlabs.tokencore.wallet.model.BIP44Util; +import org.consenlabs.tokencore.wallet.model.Metadata; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +/** + * Created by xyz on 2018/2/5. + */ + +public class V3MnemonicKeystore extends IMTKeystore implements EncMnemonicKeystore, ExportableKeystore { + private static final int VERSION = 3; + private EncPair encMnemonic; + private String mnemonicPath; + + @Override + public EncPair getEncMnemonic() { + return encMnemonic; + } + + @Override + public void setEncMnemonic(EncPair encMnemonic) { + this.encMnemonic = encMnemonic; + } + + @Override + public String getMnemonicPath() { + return this.mnemonicPath; + } + + public void setMnemonicPath(String mnemonicPath) { + this.mnemonicPath = mnemonicPath; + } + + public V3MnemonicKeystore() { + super(); + } + + public static V3MnemonicKeystore create(Metadata metadata, String password, List mnemonicCodes, String path) { + return new V3MnemonicKeystore(metadata, password, mnemonicCodes, path, ""); + } + + + private V3MnemonicKeystore(Metadata metadata, String password, List mnemonicCodes, String path, String id) { + MnemonicUtil.validateMnemonics(mnemonicCodes); + DeterministicSeed seed = new DeterministicSeed(mnemonicCodes, null, "", 0L); + DeterministicKeyChain keyChain = DeterministicKeyChain.builder().seed(seed).build(); + + this.mnemonicPath = path; + List zeroPath = BIP44Util.generatePath(path); + + byte[] prvKeyBytes = keyChain.getKeyByPath(zeroPath, true).getPrivKeyBytes(); + this.crypto = Crypto.createPBKDF2CryptoWithKDFCached(password, prvKeyBytes); + this.encMnemonic = crypto.deriveEncPair(password, Joiner.on(" ").join(mnemonicCodes).getBytes()); + this.crypto.clearCachedDerivedKey(); + + this.address = AddressCreatorManager.getInstance(metadata.getChainType(), metadata.isMainNet(), metadata.getSegWit()).fromPrivateKey(prvKeyBytes); + metadata.setTimestamp(DateUtil.getUTCTime()); + metadata.setWalletType(Metadata.V3); + this.metadata = metadata; + this.version = VERSION; + this.id = Strings.isNullOrEmpty(id) ? UUID.randomUUID().toString() : id; + } + + + + @Override + public Keystore changePassword(String oldPassword, String newPassword) { + String mnemonic = new String(getCrypto().decryptEncPair(oldPassword, this.getEncMnemonic())); + List mnemonicCodes = MnemonicUtil.toMnemonicCodes(mnemonic); + return new V3MnemonicKeystore(this.metadata, newPassword, mnemonicCodes, this.mnemonicPath, this.id); + } +} diff --git a/src/test/java/org/consenlabs/tokencore/foundation/utils/MnemonicUtilTest.java b/src/test/java/org/consenlabs/tokencore/foundation/utils/MnemonicUtilTest.java index 32c234c..95397ee 100644 --- a/src/test/java/org/consenlabs/tokencore/foundation/utils/MnemonicUtilTest.java +++ b/src/test/java/org/consenlabs/tokencore/foundation/utils/MnemonicUtilTest.java @@ -57,6 +57,20 @@ void validateMnemonics_shouldRejectInvalidWords() { assertThrows(TokenException.class, () -> MnemonicUtil.validateMnemonics(invalid)); } + @Test + void toMnemonicCodes_string_shouldTrimAndNormalizeWhitespace() { + String raw = " abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about "; + List codes = MnemonicUtil.toMnemonicCodes(raw); + assertEquals(12, codes.size()); + assertEquals("abandon", codes.get(0)); + assertEquals("about", codes.get(11)); + } + + @Test + void toMnemonicCodes_string_shouldRejectBlankInput() { + assertThrows(TokenException.class, () -> MnemonicUtil.toMnemonicCodes(" ")); + } + @Test void toMnemonicCodes_shouldConvertEntropy() { byte[] entropy = new byte[16]; diff --git a/src/test/java/org/consenlabs/tokencore/wallet/WalletManagerTest.java b/src/test/java/org/consenlabs/tokencore/wallet/WalletManagerTest.java index b044bdb..215a0ab 100644 --- a/src/test/java/org/consenlabs/tokencore/wallet/WalletManagerTest.java +++ b/src/test/java/org/consenlabs/tokencore/wallet/WalletManagerTest.java @@ -7,6 +7,7 @@ import java.io.File; import java.nio.file.Path; +import java.util.List; import static org.junit.jupiter.api.Assertions.*; @@ -77,6 +78,45 @@ void findWalletByAddress_shouldReturnWallet() { assertEquals(ethWallet.getAddress(), found.getAddress()); } + + @Test + void findWalletByKeystore_ethereum_shouldReturnWallet() { + Identity identity = Identity.createIdentity( + "test", "password123", "", Network.MAINNET, Metadata.P2WPKH); + + Wallet ethWallet = identity.deriveWalletByMnemonics( + ChainType.ETHEREUM, "password123", MnemonicUtil.randomMnemonicCodes()); + + String keystore = WalletManager.exportKeystore(ethWallet.getId(), "password123"); + Wallet found = WalletManager.findWalletByKeystore(ChainType.ETHEREUM, keystore, "password123"); + + assertNotNull(found); + assertEquals(ethWallet.getAddress(), found.getAddress()); + } + + + + @Test + void findWalletByMnemonic_dogecoin_shouldReturnWallet() { + Identity identity = Identity.createIdentity( + "test", "password123", "", Network.MAINNET, Metadata.NONE); + + List mnemonics = MnemonicUtil.randomMnemonicCodes(); + Wallet dogeWallet = identity.deriveWalletByMnemonics( + ChainType.DOGECOIN, "password123", mnemonics); + + String mnemonic = String.join(" ", mnemonics); + Wallet found = WalletManager.findWalletByMnemonic( + ChainType.DOGECOIN, + Network.MAINNET, + mnemonic, + BIP44Util.DOGECOIN_MAINNET_PATH, + Metadata.NONE); + + assertNotNull(found); + assertEquals(dogeWallet.getAddress(), found.getAddress()); + } + @Test void findWalletByAddress_notFound_shouldReturnNull() { Identity.createIdentity(