Android 以太坊 API 學(xué)習(xí)筆記 01 - 創(chuàng)建導(dǎo)入錢包

登錄 etherscan.io 可以申請得到一個 api 請求 token: https://etherscan.io/myapikey

每個賬戶最多持有 3 個 token, 請求 API service 服務(wù), 僅需其中一個即可.

官方參考文檔詳見: https://etherscan.io/apis

Gayhub 上找到一個 完整的錢包項(xiàng)目 Lunary Wallet 使用 web3j + okhttp 實(shí)現(xiàn)了 Android 以太幣錢包.
項(xiàng)目已經(jīng)上架 Google Play, 并獲得了一些好評, 代碼結(jié)構(gòu)比較整潔, 值得拿來借鑒分析.
下面基于這個項(xiàng)目分析 etherscan.io 里 API 的使用.

代碼分析

Lunary 創(chuàng)建錢包放在了 WalletGenService 中, 目前看代碼完全可以使用 web3j 封裝, 暫時不需要直接調(diào)用 etherscan.io api.

(這里通過 intent 明文傳輸密碼是一個安全隱患, 將密碼 hash 處理后再傳輸可能會好一些)

繼續(xù)追蹤內(nèi)部邏輯, 不難發(fā)現(xiàn) Service 中調(diào)用 2 個接口, 分別可以創(chuàng)建和導(dǎo)入錢包.

分別是創(chuàng)建接口 OwnWalletUtils.generateNewWalletFile 和 導(dǎo)入接口 OwnWalletUtils.generateWalletFile, 看起來沒多復(fù)雜, 主要調(diào)用 web3j 接口實(shí)現(xiàn).

// 創(chuàng)建錢包調(diào)用此方法
public static String generateNewWalletFile(
        String password, File destinationDirectory, boolean useFullScrypt)
        throws CipherException, IOException, InvalidAlgorithmParameterException,
        NoSuchAlgorithmException, NoSuchProviderException {

    ECKeyPair ecKeyPair = Keys.createEcKeyPair(); // ---------- 創(chuàng)建私鑰
    return generateWalletFile(password, ecKeyPair, destinationDirectory, useFullScrypt);
}

// 導(dǎo)入錢包調(diào)用此方法
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;
}

從傳入?yún)?shù)上看, 用戶密碼和私鑰肯定會以某種形式寫入本地文件. web3j 是如何保證本地錢包安全的呢? 看 Wallet 類內(nèi)部實(shí)現(xiàn), 具體分為以下幾步:

  1. 生成 32 位隨機(jī)字節(jié)數(shù)組 salt 對用戶密碼進(jìn)行加密 ( SCrypt 加密 ), 得到 derivedKey
  2. 取 derivedKey 前 16 位, 作為以太坊密鑰的 AES 加密密鑰 encryptKey
  3. 隨機(jī)生成 16 位隨機(jī)字節(jié)數(shù)組 iv, 加上 encryptKey 對以太坊密鑰進(jìn)行加密, 得到 cipherText
  4. 拼接 derivedKey 和 cipherText 得刀字節(jié)數(shù)組 mac, 推測是為了校驗(yàn)用
  5. 基于以上結(jié)果生成錢包實(shí)例
  6. 使用 ObjectMapper.writeValue 接口保存錢包到本地
public static WalletFile create(String password, ECKeyPair ecKeyPair, int n, int p) throws CipherException {
    byte[] salt = generateRandomBytes(32); // 引入隨機(jī)變量
    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); // 再次引入隨機(jī)變量
    byte[] privateKeyBytes = Numeric.toBytesPadded(ecKeyPair.getPrivateKey(), 32); // 幾乎還是明文
    byte[] cipherText = performCipherOperation(1, iv, encryptKey, privateKeyBytes); // AES 對稱加密密鑰
    byte[] mac = generateMac(derivedKey, cipherText); // 這步的意義沒有想明白, 推測是解密時校驗(yàn)用
    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); // --------- 后面會再次調(diào)用這個接口, 這里似乎沒有意義
    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()); // 注意這里還有一個隨機(jī)量
    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 本身不包含用戶密碼和以太坊私鑰原始數(shù)據(jù). 通過加密后的數(shù)據(jù), 理論上應(yīng)該可以通過輸入用戶密碼得到以太坊私鑰原始字節(jié)數(shù)組. 具體待后續(xù)轉(zhuǎn)賬環(huán)節(jié)代碼分析.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 創(chuàng)建一個以太坊錢包有多種方式,一般情況下可以通過geth、EtherumWallet等客戶端。對于前端,可以使用插...
    MatrixYe閱讀 16,604評論 4 26
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,695評論 19 139
  • 以太坊(Ethereum ):下一代智能合約和去中心化應(yīng)用平臺 翻譯:巨蟹 、少平 譯者注:中文讀者可以到以太坊愛...
    車圣閱讀 3,928評論 1 7
  • 今天去做了指甲 給我做彩繪的,是一位瘦瘦的短發(fā)姑娘 話并不多 極細(xì)的筆尖,在我指甲上畫著 半個巴掌大的錫紙是她的調(diào)...
    松蘿閱讀 396評論 0 1
  • 嚴(yán)明,被稱作“詩人攝影師”,冷靜、客觀卻又充滿熱情與詩意。 《大國志》是一部隨筆集和攝影集,每個人的去處都源自他的...
    土豆茉莉閱讀 635評論 0 0

友情鏈接更多精彩內(nèi)容