This article continues the blockchain-ETH process of creating a wallet. The basic concepts were outlined in the previous article. Now we start to explain how to unlock a wallet using a mnemonic, a private key, and a Keystore. For a good reading experience, read the original article

The environment

Depends on the environment or BIP family bucket

implementation 'the IO. Making. Novacrypto: BIP44:0.0.3'
    / / implementation 'IO. Making. Novacrypto: BIP32:0.0.9' / / BIP32 used in the demo BIP32 lib
implementation 'the IO. Making. Novacrypto: BIP39:0.1.9'
Copy the code

Mnemonics unlock wallets

Check the mnemonic

The mnemonic input by the user needs to be verified

        // 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();
        }
Copy the code

Unlock the purse

Mnemonic unlocking is essentially the same process as creating a wallet, but adds logic to validate duplicate wallets

    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);
                });
    }
Copy the code

Private key to unlock wallet

The private key unlocks/imports the wallet in much the same way as it was created

    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);
        });
    }
Copy the code

Keystore Unlock wallet

Keystore unlock wallet is important

Go straight to the code

    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);
                });
    }
Copy the code

The main process is to get EcKeyPair via WalletFile/Keystore + Password and then get other information. The main API is

ECKeyPair keyPair = Wallet.decrypt(password, walletFile);
Copy the code

Added logic to verify that the wallet already exists and the Keystore matches the private key

It seems that the process is so perfect, in fact, when really used, you will find that the program goes here often OOM!

Error message interception is as follows:

 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)
Copy the code

Further debugging found that when N is too large,

org.spongycastle.crypto.generators.SCrypt.SMix(..) About 124 lines in the method

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

When creating a wallet, we have to talk about our choices

Wallet.createLight(password, keyPair)
Copy the code

The use here is to create a lightweight wallet, whose original call is

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

Here N,P can be customized assignment, its meaning can be Google. Simply put, the greater the N, the more encrypted the wallet.

CreateLight (…) is called when we create the wallet. And wallets created from imToken are more customizable than our ‘lightweight’ standard, so export the Keystore from the Wallet created in __imToken__ and import it into our Wallet, calling the above web3J’s wallet.decrypt (…). Basically OOM Crash.

Web3j Issues can be found in a large number of related Issues, the answer is basically said that the dependency library is not compatible with Android. Here you can reduce the amount of time your friends have to go round in circles and offer a workable solution.

Link: Out Of Memory exception when using web3j in Android

Is that we need to modify some of the methods.

OOM optimization

There is a dependency

implementation 'com. Lambdaworks: scrypt: 1.4.0'
Copy the code

Then modify the decryption method

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);
    }
Copy the code

We need to import the corresponding so library. We create jniLibs under SRC /main and put it in the corresponding platform SO

The author has uploaded all so to Android Scrypt SO

Now the modified method lwallet.decrypt (…) is called.

    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);
                });
    }
Copy the code

Other FAQ

In the development, there will always be such a question, here is a simple answer

ImToken has the function of exporting/backing up mnemonics

A. Good question. When you create/unlock your wallet with a mnemonic, the app saves the mnemonic locally. An export simply reads the stored data out. You can try to unlock the wallet by importing the Keystore or private key on imToken, and you will find no entry to back up the mnemonic.

Q. What information does the app need to save the wallet locally

A. Theoretically, you only need to save the Keystore. Mnemonic of the wallet, and you’d better not save the private key, because once the APP is cracked, the wallet of the user can be directly obtained. If sensitive information is stored for user experience reasons, it is recommended to encrypt it symmetrically with the password entered by the user.

.

The above is the main content of ethereum to unlock the wallet, the pit in the process is basically explicitly specified.

GitHub tutorial code has been uploaded, if you need help, please click a star 🙂