區(qū)塊鏈-ETH解鎖錢包

本篇文章承接區(qū)塊鏈-ETH創(chuàng)建錢包 , 基本概念在上篇文章中已經(jīng)做了概要 , 現(xiàn)在我們開始說明分別通過助記詞,私鑰,Keystore來解鎖錢包.

為了良好的閱讀體驗, 請閱讀原文

環(huán)境

依賴環(huán)境還是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();
        }

解鎖錢包

助記詞解鎖其實與創(chuàng)建錢包過程一致,只是增加了校驗重復(fù)錢包的邏輯

    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);
                });
    }

私鑰解鎖錢包

私鑰解鎖/導(dǎo)入錢包的過程也與創(chuàng)建時大體一致

    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是否與私鑰匹配的邏輯

看似過程那么完美,其實當真正運用中就會發(fā)現(xiàn)程序走到這里經(jīng)常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)

進一步調(diào)試發(fā)現(xiàn),是因為當N過大時,

org.spongycastle.crypto.generators.SCrypt.SMix(..)方法里的 124 行左右

for (int i = 0; i < N; ++i)
{
     V[i] = Arrays.clone(X);
     ...
}

這里不停地clone,導(dǎo)致了內(nèi)存溢出Crash . 說到這里,不得不說一下創(chuàng)建錢包時,我們的選擇

Wallet.createLight(password, keyPair)

這里使用的是創(chuàng)建輕量級錢包,其原始調(diào)用為

public static WalletFile create(String password, ECKeyPair ecKeyPair, int n, int p)

這里的N ,P 是可以自定義賦值的,其意義可自行g(shù)oogle下.簡單地來說,N越大,錢包加密程度越高.

當我們創(chuàng)建錢包是調(diào)用的createLight(...) , 而從 imToken 創(chuàng)建的錢包是采用的自定義大于我們'輕量'的標準的,因此從 imToken中創(chuàng)建的錢包導(dǎo)出Keystore,再在我們的錢包中導(dǎo)入,調(diào)用上述web3j的 Wallet.decrypt(...) 基本會OOM Crash.

可以在 web3j Issues 中搜到大量相關(guān)的問題 , 解答基本是說依賴庫不兼容Android導(dǎo)致的 . 這里就減少道友們繞圈子的時間了,直接提供個可行的解決方案.

Link: Out Of Memory exception when using web3j in Android

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

OOM優(yōu)化

這里需要依賴

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 中的內(nèi)容 ,到了這里我們還需要導(dǎo)入相應(yīng)的so庫,我們在src/main下創(chuàng)建jniLibs,接著放入對應(yīng)平臺so

image

全部so筆者已上傳到 Android scrypt so

現(xiàn)在調(diào)用的是修改后的方法 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

在開發(fā)中, 總是會有這樣那樣的疑問,這里做一個簡單的答疑

__Q. 怎么導(dǎo)出助記詞啊 , imToken 有導(dǎo)出/備份助記詞的功能 . __

A. 很好的問題. 其實就是創(chuàng)建/用助記詞解鎖錢包時,app本地保存了助記詞.導(dǎo)出只是將存儲數(shù)據(jù)讀取出來而已.可以嘗試在imToken上通過導(dǎo)入Keystore或者私鑰解鎖錢包,就會發(fā)現(xiàn)沒有備份助記詞的入口.

Q. app本地需要保存錢包什么信息

A. 理論上說只需要保存錢包的Keystore.助記詞,私鑰最好別存,因為app一旦被破解,用戶的錢包就能被直接獲取到.如若有出于用戶體驗等原因保存這些敏感信息,最好結(jié)合用戶輸入的密碼做對稱加密保存.

...

以上即為以太坊解鎖錢包的主要內(nèi)容,過程中的坑基本有顯式指明.

GitHub 系列教程代碼已上傳,如果對你有所幫助,請不吝點個star :)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容