主页 > imtoken钱包转usdt > 区块链-ETH解锁钱包
区块链-ETH解锁钱包
环境
取决于环境或BIP全家桶
implementation 'io.github.novacrypto:BIP44:0.0.3'
// implementation 'io.github.novacrypto:BIP32:0.0.9' //BIP32 使用 demo中的BIP32 lib
implementation 'io.github.novacrypto:BIP39:0.1.9'
助记词 解锁钱包 验证助记词
用户输入的助记词需要验证
// validate mnemonic
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();
}
解锁钱包
助记词解锁过程其实和创建钱包过程是一样的,只是增加了校验重复钱包的逻辑
public Flowable importMnemonic(Context context,
String password,
String mnemonics) {
Flowable 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);
});
}
用于解锁钱包的私钥
用私钥解锁/导入钱包的过程也和创建时大致相同
public Flowable importPrivateKey(Context context,
String privateKey,
String password) {
if (privateKey.startsWith(Constant.PREFIX_16)) {
privateKey = privateKey.substring(Constant.PREFIX_16.length());
}
Flowable 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解锁钱包需要重点
直接上代码
public Flowable 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是
ECKeyPair keyPair = Wallet.decrypt(password, walletFile);
新增验证钱包是否已存在以及Keystore是否匹配私钥的逻辑
看似流程如此完美,其实实际使用的时候,你会发现程序来这里的时候经常OOM!
截取错误信息如下:
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 行
for (int i = 0; i < N; ++i)
{
V[i] = Arrays.clone(X);
...
}
clone被保留在这里,导致内存溢出Crash。 说到这里,就不得不说说我们在创建钱包时的选择
Wallet.createLight(password, keyPair)
这里使用的是创建一个轻量级钱包,它的原调用是
public static WalletFile create(String password, ECKeyPair ecKeyPair, int n, int p)
这里的N和P是可以自定义的,其含义可以自行google。 简单来说以太坊钱包解锁后需要多久再次解锁,N越大,钱包的加密级别越高。
我们在创建钱包的时候调用createLight(...),imToken创建的钱包采用了比我们“轻量级”标准更大的定制,所以Keystore是从imToken创建的钱包导出的,然后在我们的wallet 导入进去,调用上面web3j的Wallet.decrypt(...) 基本会导致OOM Crash。
你可以在 web3j Issues 中找到很多相关问题。 答案基本是依赖库不兼容Android。 这里是为了减少道友们兜圈子的时间,直接给出一个可行的方案。
链接:在 Android 中使用 web3j 时内存不足异常
也就是我们需要修改一些方法。
OOM优化
这里需要依赖
implementation 'com.lambdaworks:scrypt:1.4.0'
然后修改解密方式
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 = generateDerivedScryptKey(password.getBytes(Charset.forName("UTF-8")), salt, n, r, p, dklen);
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(...)
public Flowable 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);
});
}
其他常见问题
在开发中,总会遇到这类问题,这里简单回答一下
__问。 如何导出助记词,imToken有导出/备份助记词的功能。 __
答:好问题。 事实上,当你创建/使用助记词解锁钱包时,应用程序会将助记词保存在本地。 导出只是为了读取存储的数据。 你可以尝试在 imToken 上导入 Keystore 或私钥来解锁钱包,你会发现没有备份的助记词入口。
Q. App需要在钱包本地保存哪些信息?
A. 理论上你只需要保存钱包的Keystore即可。 助记词和私钥最好不要保存以太坊钱包解锁后需要多久再次解锁,因为一旦app被破解,可以直接获取用户的钱包。 如果有用户体验等原因要保存这些敏感信息最好结合用户输入的密码进行对称加密保存信息。
...
以上就是以太坊解锁钱包的主要内容,过程中的坑基本都说的很清楚了。
GitHub系列教程代码已经上传,如果对你有帮助,请不要吝啬一个star:)