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

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

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

推荐阅读更多精彩内容

  • 创建一个以太坊钱包有多种方式,一般情况下可以通过geth、EtherumWallet等客户端。对于前端,可以使用插...
    MatrixYe阅读 16,063评论 4 26
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 以太坊(Ethereum ):下一代智能合约和去中心化应用平台 翻译:巨蟹 、少平 译者注:中文读者可以到以太坊爱...
    车圣阅读 3,684评论 1 7
  • 今天去做了指甲 给我做彩绘的,是一位瘦瘦的短发姑娘 话并不多 极细的笔尖,在我指甲上画着 半个巴掌大的锡纸是她的调...
    松萝阅读 344评论 0 1
  • 严明,被称作“诗人摄影师”,冷静、客观却又充满热情与诗意。 《大国志》是一部随笔集和摄影集,每个人的去处都源自他的...
    土豆茉莉阅读 540评论 0 0