登录 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 本身不包含用户密码和以太坊私钥原始数据. 通过加密后的数据, 理论上应该可以通过输入用户密码得到以太坊私钥原始字节数组. 具体待后续转账环节代码分析.

更多相关文章

  1. android发布过程
  2. Android中是用Base64进行加密解密
  3. Android客户端与服务器端通过DES加密认证
  4. MD5
  5. [Android(安卓)Pro] AES加密
  6. Android(安卓)DES加密解密算法
  7. Android(安卓)RSA 公钥加密、解密
  8. Android(安卓)md5加密与php md5加密一致详解
  9. android 以太网和wifi共存 记号

随机推荐

  1. 如何.abort()ajax文件上传?
  2. JavaScript获取某年某月的最后一天
  3. 如何测试潜在的“浏览器崩溃”JavaScript
  4. Cesium学习笔记(八):Demo学习(差值器)
  5. 将(重度公式加载的)Excel电子表格转换为用
  6. 从终端(iOS)运行React Native App出错
  7. 有没有办法用JavaScript控制iPod Touch的
  8. 将HTML返回到AJAX Rails调用
  9. 什么是计算Web KSLOC的好工具?
  10. jquery中的mouseenter实现理解