借助網上的一段描述:
若以銀行賬戶為類比,這 5 個詞分別對應內容如下:
地址=銀行卡號
密碼=銀行卡密碼
私鑰=銀行卡號+銀行卡密碼
助記詞=銀行卡號+銀行卡密碼
Keystore+密碼=銀行卡號+銀行卡密碼
Keystore ≠ 銀行卡號
1 2
|
implementation 'org.web3j:core:3.3.1-android' implementation 'io.github.novacrypto:BIP39:0.1.9'
|
org.web3j:core
這個庫是Java的,org.web3j:core:x-android
是兼容Android平台,所有接口和工具類都是為Java應用設計的,所以在Android上使用的時候要注意變通一下。
創建數字身份
創建錢包身份可以通過 WalletUtils 類來實現,它可以創建兩種錢包:標准和 BIP39。
可以通過 generateWalletFile 函數創建,直接保存為json文件,以下其他三個函數都是它的封裝。
在Android上不建議使用 WalletUtils
的這幾個函數創建數字身份。
1 2 3 4
|
WalletUtils.generateFullNewWalletFile(); WalletUtils.generateLightNewWalletFile(); WalletUtils.generateNewWalletFile(); WalletUtils.generateWalletFile();
|
generateFullNewWalletFile 使用N_STANDARD加密強度,在Android上會發送OOM,Android的處理速度也跟不上。
generateLightNewWalletFile 相對來說比較輕量級,但是在我手機(紅米4)上也花了21秒才創建完成,而加載為 Credentials 花了40秒。而在一台三星手機跑比較快,7秒左右。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
public void (View view) {
try { String password = "123456"; String path = Environment.getExternalStorageDirectory().getPath() + "/MyWallet"; File fileDir = new File(path); if (!fileDir.exists()) { fileDir.mkdirs(); } String fileName = null;
Log.e(TAG, "startClickCreateDefault: "+fileDir.getPath() ); fileName = WalletUtils.generateLightNewWalletFile("123456",fileDir);
Log.e(TAG, "wallet fileName: " + fileName );
Credentials credentials = WalletUtils.loadCredentials(password,path + "/" +fileName); Log.e(TAG, "getAddress: "+credentials.getAddress()); Log.e(TAG, "getPrivateKey: "+credentials.getEcKeyPair().getPrivateKey()); Log.e(TAG, "getPublicKey: "+credentials.getEcKeyPair().getPublicKey()); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (NoSuchProviderException e) { e.printStackTrace(); } catch (InvalidAlgorithmParameterException e) { e.printStackTrace(); } catch (CipherException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }
|
創建BIP39身份
可以導出助記詞,創建花了43秒,用助記詞導入很快,只花了幾秒。
1
|
WalletUtils.generateBip39Wallet();
|
WalletUtils提供的這個方法在Android上閃退,只有自己寫一個了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
|
public void startClickCreateBip39(View view){
String password = "123456"; String path = Environment.getExternalStorageDirectory().getPath() + "/MyWallet"; File fileDir = new File(path); if (!fileDir.exists()) { fileDir.mkdirs(); } Log.e(TAG, "wallet start"); try {
StringBuilder sb = new StringBuilder(); byte[] entropy = new byte[Words.TWELVE.byteLength()]; new SecureRandom().nextBytes(entropy); new MnemonicGenerator(English.INSTANCE).createMnemonic(entropy, sb::append); String mnemonic = sb.toString(); byte[] seed = MnemonicUtils.generateSeed(mnemonic, password); ECKeyPair privateKey = ECKeyPair.create(sha256(seed));
String fileName = WalletUtils.generateWalletFile(password, privateKey, fileDir, false); Log.e(TAG, "fileName: " + fileName); Log.e(TAG, "助記詞wallet.getMnemonic(): " + mnemonic); Credentials credentials = WalletUtils.loadBip39Credentials(password,mnemonic); Log.e(TAG, "getAddress: "+credentials.getAddress()); Log.e(TAG, "getPrivateKey: "+credentials.getEcKeyPair().getPrivateKey()); Log.e(TAG, "getPublicKey: "+credentials.getEcKeyPair().getPublicKey());
} catch (CipherException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (Exception e){ e.printStackTrace(); }
}
|
Android創建錢包
上面這些感覺比較適合Java程序,我們跳進去看看就知道了,其實生成數字身份的代碼是:
1 2 3 4
|
public static WalletFile createLight(String password, ECKeyPair ecKeyPair) throws CipherException { return create(password, ecKeyPair, N_LIGHT, P_LIGHT); }
|
針對Android,我們需要將生成的數字身份 WalletFile
轉為 JSON
(Keystore)保存到 SharedPreferences
,所以整理一個工具類:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 大專欄 14、創建/恢復ETH錢包身份s="line">80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
|
import android.support.annotation.Nullable; import android.util.Log;
import org.web3j.crypto.CipherException; import org.web3j.crypto.Credentials; import org.web3j.crypto.ECKeyPair; import org.web3j.crypto.Keys; import org.web3j.crypto.MnemonicUtils; import org.web3j.crypto.Wallet; import org.web3j.crypto.WalletFile;
import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.SecureRandom;
import io.github.novacrypto.bip39.MnemonicGenerator; import io.github.novacrypto.bip39.Words; import io.github.novacrypto.bip39.wordlists.English;
import static org.web3j.crypto.Hash.sha256;
public class MyWalletTool {
private final String TAG = getClass().getName();
public WalletFile createLightWallet(String password){
WalletFile walletFile = null; try { walletFile = Wallet.createLight(password, Keys.createEcKeyPair()); } catch (CipherException | NoSuchProviderException | NoSuchAlgorithmException | InvalidAlgorithmParameterException e) { e.printStackTrace(); } return walletFile; }
public String createMnemonic(){
StringBuilder sb = new StringBuilder(); byte[] entropy = new byte[Words.TWELVE.byteLength()]; new SecureRandom().nextBytes(entropy); new MnemonicGenerator(English.INSTANCE).createMnemonic(entropy, sb::append); return sb.toString(); }
public WalletFile createBip39Wallet(String password,String mnemonic){
WalletFile walletFile = null; try { byte[] seed = MnemonicUtils.generateSeed(mnemonic, password); Log.d(TAG, "createLight start..."); walletFile = Wallet.createLight(password, ECKeyPair.create(sha256(seed))); Log.d(TAG, "createLight end."); } catch (Exception e){ e.printStackTrace(); } return walletFile; }
public Credentials createCredentials(String password,String mnemonic){
byte[] seed = MnemonicUtils.generateSeed(mnemonic, password); return Credentials.create(ECKeyPair.create(sha256(seed))); }
public Credentials createCredentials(String password,WalletFile walletFile) throws CipherException {
return Credentials.create(Wallet.decrypt(password, walletFile)); }
public Credentials createCredentials(String privateKey) {
return Credentials.create(privateKey); }
|
交易憑證Credentials
在 web3j
中每一個交易都需要一個參數:Credentials
,Credentials
實例化有三種方法,其中私鑰權限最高,所以絕不能泄露自己的私鑰和助記詞,常用的是密碼 + Keystore
。
從MyWalletTool
調用的函數來看,交易憑證的實例化只需要以下之一:
- 私鑰
- 助記詞
- 密碼 + Keystore
私鑰
一個錢包只有一個私鑰且不能修改
為什么 私鑰
單獨可以實現實例化 Credentials
?
Credentials
的構造函數參數是 ECKeyPair
和 address
1 2 3 4
|
private Credentials(ECKeyPair ecKeyPair, String address) { this.ecKeyPair = ecKeyPair; this.address = address; }
|
address
可以通過 ECKeyPair
推導出來,而 ECKeyPair
的構造函數參數就是公鑰和私鑰
1 2 3 4
|
public ECKeyPair(BigInteger privateKey, BigInteger publicKey) { this.privateKey = privateKey; this.publicKey = publicKey; }
|
公鑰可以通過私鑰推導出來,所以可以直接實例化 Credentials
。
1
|
Sign.publicKeyFromPrivate(privateKey)
|
助記詞
助記詞是明文私鑰的另一種表現形式,其目的是為了幫助用戶記憶復雜的私鑰
Canache生成的一個助記詞
1 2 3
|
助記詞:jump dolphin leave reward allow farm gate hospital region diary seminar loan 地址:0x7E728c371D66813434F340E6D473B212F506bA54 私鑰:6229413033912ab1f26e36f0aad7e1ea2b957de73cfedf788b9fff811192aa89
|
用 imToken
可以成功導入錢包,但是用下面的 BIP39
標准的代碼卻不行(passphrase是加鹽,這里為空)。
1 2 3 4
|
byte[] seed = MnemonicUtils.generateSeed(mnemonic, passphrase); ECKeyPair ecKeyPair = ECKeyPair.create(sha256(seed)); System.out.println("private=" + ecKeyPair.getPrivateKey().toString()); System.out.println("private=" + ecKeyPair.getPrivateKey().toString(16));
|
結果是:
1 2
|
private=27538423023524426157929608133615570842335693203949154557762660148101331275721 private=3ce231f097447fe5d623b3a1f9a37e8c554ee014959903c4e2ebadf69ac7cfc9
|
網上查資料說 imToken
用的是 BIP44
標准。后面再看看怎么搞,imToken核心碼開源地址
BIP44助記詞創建和導入
Keystore
將私鑰以加密的方式保存為一份 JSON 文件,這份 JSON 文件就是 Keystore,所以它就是加密后的私鑰,它必須配合錢包密碼才能使用該賬號。
1
|
ECKeyPair ecKeyPair = Wallet.decrypt(password, walletFile);
|
錢包開源項目
- A ethereum wallet like imToken for Android
- A beautiful, secure and native Ethereum Wallet for Android
- Lightweight JS Wallet for Node and the browser
- A plugin that turns Vault into an Ethereum wallet. Golang