美文网首页以太坊以太坊开发笔记Dapp开发
Android 以太坊 API 学习笔记 01 - 创建导入钱包

Android 以太坊 API 学习笔记 01 - 创建导入钱包

作者: 古月XYZ | 来源:发表于2018-06-04 11:26 被阅读803次

    登录 etherscan.io 可以申请得到一个 api 请求 token: https://etherscan.io/myapikey

    每个账户最多持有 3 个 token, 请求 API service 服务, 仅需其中一个即可.

    官方参考文档详见: https://etherscan.io/apis

    Gayhub 上找到一个 完整的钱包项目 Lunary Wallet 使用 web3j + okhttp 实现了 Android 以太币钱包.
    项目已经上架 Google Play, 并获得了一些好评, 代码结构比较整洁, 值得拿来借鉴分析.
    下面基于这个项目分析 etherscan.io 里 API 的使用.

    代码分析

    Lunary 创建钱包放在了 WalletGenService 中, 目前看代码完全可以使用 web3j 封装, 暂时不需要直接调用 etherscan.io api.

    (这里通过 intent 明文传输密码是一个安全隐患, 将密码 hash 处理后再传输可能会好一些)

    继续追踪内部逻辑, 不难发现 Service 中调用 2 个接口, 分别可以创建和导入钱包.

    分别是创建接口 OwnWalletUtils.generateNewWalletFile 和 导入接口 OwnWalletUtils.generateWalletFile, 看起来没多复杂, 主要调用 web3j 接口实现.

    // 创建钱包调用此方法
    public static String generateNewWalletFile(
            String password, File destinationDirectory, boolean useFullScrypt)
            throws CipherException, IOException, InvalidAlgorithmParameterException,
            NoSuchAlgorithmException, NoSuchProviderException {
    
        ECKeyPair ecKeyPair = Keys.createEcKeyPair(); // ---------- 创建私钥
        return generateWalletFile(password, ecKeyPair, destinationDirectory, useFullScrypt);
    }
    
    // 导入钱包调用此方法
    public static String generateWalletFile(
            String password, ECKeyPair ecKeyPair, File destinationDirectory, boolean useFullScrypt)
            throws CipherException, IOException {
    
        WalletFile walletFile; // ---------- web3j 提供的钱包文件类, 推荐阅读源码
        if (useFullScrypt) {
            walletFile = Wallet.createStandard(password, ecKeyPair);
        } else {
            walletFile = Wallet.createLight(password, ecKeyPair);
        }
    
        String fileName = getWalletFileName(walletFile);
        File destination = new File(destinationDirectory, fileName);
    
        ObjectMapper objectMapper = ObjectMapperFactory.getObjectMapper();
        objectMapper.writeValue(destination, walletFile);
    
        return fileName;
    }
    

    从传入参数上看, 用户密码和私钥肯定会以某种形式写入本地文件. web3j 是如何保证本地钱包安全的呢? 看 Wallet 类内部实现, 具体分为以下几步:

    1. 生成 32 位随机字节数组 salt 对用户密码进行加密 ( SCrypt 加密 ), 得到 derivedKey
    2. 取 derivedKey 前 16 位, 作为以太坊密钥的 AES 加密密钥 encryptKey
    3. 随机生成 16 位随机字节数组 iv, 加上 encryptKey 对以太坊密钥进行加密, 得到 cipherText
    4. 拼接 derivedKey 和 cipherText 得刀字节数组 mac, 推测是为了校验用
    5. 基于以上结果生成钱包实例
    6. 使用 ObjectMapper.writeValue 接口保存钱包到本地
    public static WalletFile create(String password, ECKeyPair ecKeyPair, int n, int p) throws CipherException {
        byte[] salt = generateRandomBytes(32); // 引入随机变量
        byte[] derivedKey = generateDerivedScryptKey(password.getBytes(Charset.forName("UTF-8")), salt, n, 8, p, 32); // 加密用户密码
        byte[] encryptKey = Arrays.copyOfRange(derivedKey, 0, 16);
        byte[] iv = generateRandomBytes(16); // 再次引入随机变量
        byte[] privateKeyBytes = Numeric.toBytesPadded(ecKeyPair.getPrivateKey(), 32); // 几乎还是明文
        byte[] cipherText = performCipherOperation(1, iv, encryptKey, privateKeyBytes); // AES 对称加密密钥
        byte[] mac = generateMac(derivedKey, cipherText); // 这步的意义没有想明白, 推测是解密时校验用
        return createWalletFile(ecKeyPair, cipherText, iv, salt, mac, n, p);
    }
    
    public static WalletFile createStandard(String password, ECKeyPair ecKeyPair) throws CipherException {
        return create(password, ecKeyPair, 262144, 1);
    }
    
    public static WalletFile createLight(String password, ECKeyPair ecKeyPair) throws CipherException {
        return create(password, ecKeyPair, 4096, 6);
    }
    
    private static WalletFile createWalletFile(ECKeyPair ecKeyPair, byte[] cipherText, byte[] iv, byte[] salt, byte[] mac, int n, int p) {
        WalletFile walletFile = new WalletFile();
        walletFile.setAddress(Keys.getAddress(ecKeyPair));
        Crypto crypto = new Crypto();
        crypto.setCipher("aes-128-ctr");
        crypto.setCiphertext(Numeric.toHexStringNoPrefix(cipherText));
        walletFile.setCrypto(crypto); // --------- 后面会再次调用这个接口, 这里似乎没有意义
        CipherParams cipherParams = new CipherParams();
        cipherParams.setIv(Numeric.toHexStringNoPrefix(iv));
        crypto.setCipherparams(cipherParams);
        crypto.setKdf("scrypt");
        ScryptKdfParams kdfParams = new ScryptKdfParams();
        kdfParams.setDklen(32);
        kdfParams.setN(n);
        kdfParams.setP(p);
        kdfParams.setR(8);
        kdfParams.setSalt(Numeric.toHexStringNoPrefix(salt));
        crypto.setKdfparams(kdfParams);
        crypto.setMac(Numeric.toHexStringNoPrefix(mac));
        walletFile.setCrypto(crypto); // ---------- 这里覆盖了上次 setCrypto
        walletFile.setId(UUID.randomUUID().toString()); // 注意这里还有一个随机量
        walletFile.setVersion(3);
        return walletFile;
    }
    
    private static byte[] generateDerivedScryptKey(byte[] password, byte[] salt, int n, int r, int p, int dkLen) throws CipherException {
        try {
            return SCrypt.scrypt(password, salt, n, r, p, dkLen); // SCrypt 是一种针对密码的加密方法, 参考 wiki: https://en.wikipedia.org/wiki/Scrypt
        } catch (GeneralSecurityException var7) {
            throw new CipherException(var7);
        }
    }
    
    private static byte[] performCipherOperation(int mode, byte[] iv, byte[] encryptKey, byte[] text) throws CipherException {
        try {
            IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
            Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
            SecretKeySpec secretKeySpec = new SecretKeySpec(encryptKey, "AES");
            cipher.init(mode, secretKeySpec, ivParameterSpec);
            return cipher.doFinal(text);
        } catch (NoSuchPaddingException var7) {
            return throwCipherException(var7);
        } catch (NoSuchAlgorithmException var8) {
            return throwCipherException(var8);
        } catch (InvalidAlgorithmParameterException var9) {
            return throwCipherException(var9);
        } catch (InvalidKeyException var10) {
            return throwCipherException(var10);
        } catch (BadPaddingException var11) {
            return throwCipherException(var11);
        } catch (IllegalBlockSizeException var12) {
            return throwCipherException(var12);
        }
    }
    
    private static byte[] generateMac(byte[] derivedKey, byte[] cipherText) {
        byte[] result = new byte[16 + cipherText.length];
        System.arraycopy(derivedKey, 16, result, 0, 16);
        System.arraycopy(cipherText, 0, result, 16, cipherText.length);
        return Hash.sha3(result);
    }
    

    从代码上看, WalletFile 本身不包含用户密码和以太坊私钥原始数据. 通过加密后的数据, 理论上应该可以通过输入用户密码得到以太坊私钥原始字节数组. 具体待后续转账环节代码分析.

    相关文章

      网友评论

      • 肖义熙:在调用这段代码的时候OOM了,请问是怎么回事呢?
        Wallet.createStandard(password, ecKeyPair);
        这个是生成钱包地址的吧 ?以太坊生成地址的时候必须有一个密码么?求解答~
        唠嗑008:换成轻钱包。Wallet.createLight(password, keyPair);
        洋洋_7985:我也是这步内存溢出了,调的OwnWalletUtils.generateNewWalletFile(),秘钥不是ECKeyPair ecKeyPair = Keys.createEcKeyPair();这句已经生成了秘钥了呀
        古月XYZ:@肖义熙 必须要有个密钥, 这个对应相应地址的操作权
      • 编程狂魔:楼主分享一个我写的java开发以太坊区块链的教程,web3j开发详解:
        http://xc.hubwiz.com/course/5b2b6e82c02e6b6a59171de2?affid=623jianshu,多提意见。
        古月XYZ:@编程狂魔 没问题. 私聊?
        编程狂魔:@古月XYZ 是想看看楼主对我们这种形式是否感兴趣?是否有意合作合作,写个课程?
        古月XYZ:@编程狂魔 emmm, 广告?
      • 7ad693b0fc7e:编译报错:
        Process 'command 'D:\AndroidStudioSdk\ndk-bundle\ndk-build.cmd'' finished with non-zero exit value 2
        请问如何解决呢?
        ZZ_d8ce:@古月XYZ E:\androidSDK\sdk1122-lite\ndk-bundle 这样没错吧
        ZZ_d8ce:@古月XYZ 检查了,ndk环境没问题呀,还是报这个错
        古月XYZ:检查一下 NDK 相关环境配置
      • 心事重重啦啦啦啦:PrivateKeyActivity里面有一个getPrivateKey方法,在这里面生成的明文私钥,然后我在自己的项目里面调用了,一直报No such file or directory 文件没找到这个错误,你看下你有遇到么
        心事重重啦啦啦啦:@古月XYZ 是少加了个写入文件动态权限,现在已经搞定了
        古月XYZ:没有遇到过。我猜你可能没有调用 generateWalletFile 生成钱包文件。
      • 心事重重啦啦啦啦:创建钱包的时候PRIVATE_KEY这个值是 如何生成的呢?
        古月XYZ:这里也是我一直疑惑的地方:从代码上看, 创建私钥和地址完全在本地通过随机策略实现, 那和已有地址碰撞了怎么办。。。如果你理清了这块逻辑, 欢迎回复
        古月XYZ:参考创建新钱包的代码: ECKeyPair ecKeyPair = Keys.createEcKeyPair(); // ---------- 创建私钥
      • 心事重重啦啦啦啦:问一下,我要做钱包项目,就必须要在 etherscan.io上申请api请求的token么
        古月XYZ:@eb1f7190fb2b etherscan.io 是个免费的解决方案, token 申请也很方便。 理论上也可以自己搭建个服务器做同样的事, 只是我觉得没有必要。

      本文标题:Android 以太坊 API 学习笔记 01 - 创建导入钱包

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