本篇文章承接区块链-ETH创建钱包 , 基本概念在上篇文章中已经做了概要 , 现在我们开始说明分别通过助记词,私钥,Keystore来解锁钱包.
为了良好的阅读体验, 请阅读原文
环境 依赖环境还是BIP全家桶
1 2 3 4 implementation 'io.github.novacrypto:BIP44:0.0.3' implementation 'com.lhalcyon:bip32:1.0.0' implementation 'io.github.novacrypto:BIP39:0.1.9'
助记词解锁钱包 校验助记词 对用户输入的助记词需要进行校验
1 2 3 4 5 6 7 8 9 10 11 12 try { MnemonicValidator.ofWordList(English.INSTANCE).validate(mnemonics); } catch (InvalidChecksumException e) { e.printStackTrace(); } catch (InvalidWordCountException e) { e.printStackTrace(); } catch (WordNotFoundException e) { e.printStackTrace(); } catch (UnexpectedWhiteSpaceException e) { e.printStackTrace(); }
解锁钱包 助记词解锁其实与创建钱包过程一致,只是增加了校验重复钱包的逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public Flowable<HLWallet> importMnemonic (Context context, String password, String mnemonics) { Flowable<String> flowable = Flowable.just(mnemonics); return flowable .flatMap(s -> { ECKeyPair keyPair = generateKeyPair(s); WalletFile walletFile = Wallet.createLight(password, keyPair); HLWallet hlWallet = new HLWallet (walletFile); if (WalletManager.shared().isWalletExist(hlWallet.getAddress())) { return Flowable.error(new HLError (ReplyCode.walletExisted, new Throwable ("Wallet existed!" ))); } WalletManager.shared().saveWallet(context, hlWallet); return Flowable.just(hlWallet); }); }
私钥解锁钱包 私钥解锁/导入钱包的过程也与创建时大体一致
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public Flowable<HLWallet> importPrivateKey (Context context, String privateKey, String password) { if (privateKey.startsWith(Constant.PREFIX_16)) { privateKey = privateKey.substring(Constant.PREFIX_16.length()); } Flowable<String> flowable = Flowable.just(privateKey); return flowable.flatMap(s -> { byte [] privateBytes = Hex.decode(s); ECKeyPair ecKeyPair = ECKeyPair.create(privateBytes); WalletFile walletFile = Wallet.createLight(password, ecKeyPair); HLWallet hlWallet = new HLWallet (walletFile); if (WalletManager.shared().isWalletExist(hlWallet.getAddress())) { return Flowable.error(new HLError (ReplyCode.walletExisted, new Throwable ("Wallet existed!" ))); } WalletManager.shared().saveWallet(context, hlWallet); return Flowable.just(hlWallet); }); }
Keystore解锁钱包 Keystore解锁钱包需要重点来讲
直接先上代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public Flowable<HLWallet> importKeystoreViaWeb3j (Context context, String keystore, String password) { return Flowable.just(keystore) .flatMap(s -> { ObjectMapper objectMapper = new ObjectMapper (); WalletFile walletFile = objectMapper.readValue(keystore, WalletFile.class); ECKeyPair keyPair = Wallet.decrypt(password, walletFile); HLWallet hlWallet = new HLWallet (walletFile); WalletFile generateWalletFile = Wallet.createLight(password, keyPair); if (!generateWalletFile.getAddress().equalsIgnoreCase(walletFile.getAddress())) { return Flowable.error(new HLError (ReplyCode.failure, new Throwable ("address doesn't match private key" ))); } if (WalletManager.shared().isWalletExist(hlWallet.getAddress())) { return Flowable.error(new HLError (ReplyCode.walletExisted, new Throwable ("Wallet existed!" ))); } WalletManager.shared().saveWallet(context, hlWallet); return Flowable.just(hlWallet); }); }
其过程主要是通过 WalletFile / Keystore + Password
得到 EcKeyPair
接着得到其他信息,主要API为
1 ECKeyPair keyPair = Wallet.decrypt(password, walletFile);
增加了校验钱包是否已存在,以及Keystore是否与私钥匹配的逻辑
看似过程那么完美,其实当真正运用中就会发现程序走到这里经常OOM!
报错信息截取如下:
1 2 3 4 5 at org.spongycastle.crypto.generators.SCrypt.SMix(SCrypt.java:143) at org.spongycastle.crypto.generators.SCrypt.MFcrypt(SCrypt.java:87) at org.spongycastle.crypto.generators.SCrypt.generate(SCrypt.java:66) at org.web3j.crypto.Wallet.generateDerivedScryptKey(Wallet.java:136) at org.web3j.crypto.Wallet.decrypt(Wallet.java:214)
进一步调试发现,是因为当N
过大时,
org.spongycastle.crypto.generators.SCrypt.SMix(..)
方法里的 124 行左右
1 2 3 4 5 for (int i = 0 ; i < N; ++i){ V[i] = Arrays.clone(X); ... }
这里不停地clone,导致了内存溢出Crash . 说到这里,不得不说一下创建钱包时,我们的选择
1 Wallet.createLight(password, keyPair)
这里使用的是创建轻量级钱包,其原始调用为
1 public static WalletFile create (String password, ECKeyPair ecKeyPair, int n, int p)
这里的N
, P
是可以自定义赋值的,其意义可自行google下.简单地来说,N
越大,钱包加密程度越高.
当我们创建钱包是调用的createLight(...)
, 而从 imToken 创建的钱包是采用的自定义大于我们’轻量’的标准的,因此从 __imToken__中创建的钱包导出Keystore,再在我们的钱包中导入,调用上述web3j的 Wallet.decrypt(...)
基本会OOM Crash.
可以在 web3j Issues 中搜到大量相关的问题 , 解答基本是说依赖库不兼容Android导致的 . 这里就减少道友们绕圈子的时间了,直接提供个可行的解决方案.
Link: Out Of Memory exception when using web3j in Android
就是我们需要修改部分方法.
OOM优化 这里需要依赖
1 implementation 'com.lambdaworks:scrypt:1.4.0'
然后修改解密方法
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 public static ECKeyPair decrypt (String password, WalletFile walletFile) throws CipherException { validate(walletFile); WalletFile.Crypto crypto = walletFile.getCrypto(); byte [] mac = Numeric.hexStringToByteArray(crypto.getMac()); byte [] iv = Numeric.hexStringToByteArray(crypto.getCipherparams().getIv()); byte [] cipherText = Numeric.hexStringToByteArray(crypto.getCiphertext()); byte [] derivedKey; if (crypto.getKdfparams() instanceof WalletFile.ScryptKdfParams) { WalletFile.ScryptKdfParams scryptKdfParams = (WalletFile.ScryptKdfParams) crypto.getKdfparams(); int dklen = scryptKdfParams.getDklen(); int n = scryptKdfParams.getN(); int p = scryptKdfParams.getP(); int r = scryptKdfParams.getR(); byte [] salt = Numeric.hexStringToByteArray(scryptKdfParams.getSalt()); derivedKey = com.lambdaworks.crypto.SCrypt.scryptN(password.getBytes(Charset.forName("UTF-8" )), salt, n, r, p, dklen); } else if (crypto.getKdfparams() instanceof WalletFile.Aes128CtrKdfParams) { WalletFile.Aes128CtrKdfParams aes128CtrKdfParams = (WalletFile.Aes128CtrKdfParams) crypto.getKdfparams(); int c = aes128CtrKdfParams.getC(); String prf = aes128CtrKdfParams.getPrf(); byte [] salt = Numeric.hexStringToByteArray(aes128CtrKdfParams.getSalt()); derivedKey = generateAes128CtrDerivedKey( password.getBytes(Charset.forName("UTF-8" )), salt, c, prf); } else { throw new CipherException ("Unable to deserialize params: " + crypto.getKdf()); } byte [] derivedMac = generateMac(derivedKey, cipherText); if (!Arrays.equals(derivedMac, mac)) { throw new CipherException ("Invalid password provided" ); } byte [] encryptKey = Arrays.copyOfRange(derivedKey, 0 , 16 ); byte [] privateKey = performCipherOperation(Cipher.DECRYPT_MODE, iv, encryptKey, cipherText); return ECKeyPair.create(privateKey); }
注释的代码行为 web3j 中的内容 ,到了这里我们还需要导入相应的so库,我们在src/main
下创建jniLibs
,接着放入对应平台so
全部so笔者已上传到 Android scrypt so
现在调用的是修改后的方法 LWallet.decrypt(...)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public Flowable<HLWallet> importKeystore (Context context, String keystore, String password) { return Flowable.just(keystore) .flatMap(s -> { ObjectMapper objectMapper = new ObjectMapper (); WalletFile walletFile = objectMapper.readValue(keystore, WalletFile.class); ECKeyPair keyPair = LWallet.decrypt(password, walletFile); HLWallet hlWallet = new HLWallet (walletFile); WalletFile generateWalletFile = Wallet.createLight(password, keyPair); if (!generateWalletFile.getAddress().equalsIgnoreCase(walletFile.getAddress())) { return Flowable.error(new HLError (ReplyCode.failure, new Throwable ("address doesn't match private key" ))); } if (WalletManager.shared().isWalletExist(hlWallet.getAddress())) { return Flowable.error(new HLError (ReplyCode.walletExisted, new Throwable ("Wallet existed!" ))); } WalletManager.shared().saveWallet(context, hlWallet); return Flowable.just(hlWallet); }); }
Other FAQ 在开发中, 总是会有这样那样的疑问,这里做一个简单的答疑
__Q. 怎么导出助记词啊 , imToken 有导出/备份助记词的功能 . __
A. 很好的问题. 其实就是创建/用助记词解锁钱包时,app本地保存了助记词.导出只是将存储数据读取出来而已.可以尝试在imToken上通过导入Keystore或者私钥解锁钱包,就会发现没有备份助记词的入口.
Q. app本地需要保存钱包什么信息
A. 理论上说只需要保存钱包的Keystore.助记词,私钥最好别存,因为app一旦被破解,用户的钱包就能被直接获取到.如若有出于用户体验等原因保存这些敏感信息,最好结合用户输入的密码做对称加密保存.
…
以上即为以太坊解锁钱包的主要内容,过程中的坑基本有显式指明.
GitHub 系列教程代码已上传,如果对你有所帮助,请不吝点个star :)