区块链-ETH解锁钱包

本篇文章承接区块链-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 :)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容