区块链-ETH解锁钱包

作者: Lmaoshammy | 来源:发表于2018-06-23 14:25 被阅读25次

    本篇文章承接区块链-ETH创建钱包 , 基本概念在上篇文章中已经做了概要 , 现在我们开始说明分别通过助记词,私钥,Keystore来解锁钱包.

    为了良好的阅读体验, 请阅读原文

    环境

    依赖环境还是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<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);
                    });
        }
    

    私钥解锁钱包

    私钥解锁/导入钱包的过程也与创建时大体一致

        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解锁钱包需要重点来讲

    直接先上代码

        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为

    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 创建的钱包是采用的自定义大于我们'轻量'的标准的,因此从 imToken中创建的钱包导出Keystore,再在我们的钱包中导入,调用上述web3j的 Wallet.decrypt(...) 基本会OOM Crash.

    可以在 web3j Issues 中搜到大量相关的问题 , 解答基本是说依赖库不兼容Android导致的 . 这里就减少道友们绕圈子的时间了,直接提供个可行的解决方案.

    Link: Out Of Memory exception when using web3j in Android

    就是我们需要修改部分方法.

    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

    image

    全部so笔者已上传到 Android scrypt so

    现在调用的是修改后的方法 LWallet.decrypt(...)

        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 :)

    相关文章

      网友评论

      • 唠嗑008:你好,请问如何生成KeyStore?

      本文标题:区块链-ETH解锁钱包

      本文链接:https://www.haomeiwen.com/subject/dvgryftx.html