本文介绍以太坊(Ethereum)钱包的原理以及创建过程,依据web3j库实现。
名词解释:
对称加密对称加密算法也就是加密和解密用相同的密钥。这种加密方式加密速度非常快,适合经常发送数据的场合。缺点是密钥的传输比较麻烦。
非对称加密加密和解密用的密钥是不同的,这种加密方式是用数学上的难解问题构造的,通常加密解密的速度比较慢,适合偶尔发送数据的场合。优点是密钥传输方便。常见的非对称加密算法为RSA、ECC和EIGamal。
AES高级加密标准(AES,Advanced Encryption Standard)为最常见的对称加密算法。
PBKDF2(Password-Based Key Derivation Function)是一个用来导出密钥的函数,常用于生成加密的密码。
它的基本原理是通过一个伪随机函数(例如HMAC函数),把明文和一个盐值作为输入参数,然后重复进行运算,并最终产生密钥。
钱包创建
钱包创建需要一个非对称加密密钥对,以太坊选择的是椭圆曲线加密算法(ECC)中的Secp256k1(依据速度、安全性等参数)。
步骤:
- 根据Secp256k1生成256位公钥/密钥,然后编译成长度为64位的十六进制字符串。
- 生成公钥的keccak-256(一种HSA-3算法)哈希值。此时为256位二进制数字。
- 丢弃前面96位,即12字节。得到160位二机制数据,即20字节。
- 把20字节编译成十六进制的字符串。得到40字符的字符串,这就是账户地址。
5.账户生成后被写成一个keystore文件,文件名就是钱包地址。
我们采用的是web3j库。流程图:
直接上代码。
//Keys.java生成Secp256k1公钥/密钥对,并存储到ECKeyPair内
public static ECKeyPair createEcKeyPair(){
KeyPair keyPair = createSecp256k1KeyPair();
return ECKeyPair.create(keyPair);
}
//Keys.java根据Keccak算法加密公钥并截取最后160位
public static String getAddress(String publicKey) {
String publicKeyNoPrefix = Numeric.toHexStringWithPrefixZeroPadded(publicKey, PUBLIC_KEY_LENGTH_IN_HEX))
…
//sha3即采用Keccak算法
String hash = Hash.sha3(publicKeyNoPrefix);
//取右方160bits,也就是40个字符的16进制
return hash.substring(hash.length() - ADDRESS_LENGTH_IN_HEX);
}
//密码加密,具体采用的算法是“HmacSHA256”
private static byte[] generateDerivedScryptKey(
byte[] password, byte[] salt, int n, int r, int p, int dkLen) throws CipherException {
...
return SCrypt.scrypt(password, salt, n, r, p, dkLen);
...
}
public static WalletFile create(String password, ECKeyPair ecKeyPair, int n, int p)
throws CipherException {
//salt参数
byte[] salt = generateRandomBytes(32);
//将密码用scrypt算法加密并生成32位密码,此加密是为了防止用户输入的密码强度过低导致不安全
byte[] derivedKey = generateDerivedScryptKey(
password.getBytes(Charset.forName("UTF-8")), salt, n, R, p, 32);
//截取加密后的密码前16位
byte[] encryptKey = Arrays.copyOfRange(derivedKey, 0, 16);
//AES加密时的16字节初始化向量
byte[] iv = generateRandomBytes(16);
//私钥转成Byte
byte[] privateKeyBytes =
Numeric.toBytesPadded(ecKeyPair.getPrivateKey(), Keys.PRIVATE_KEY_SIZE);
//根据密码加密私钥
byte[] cipherText = performCipherOperation(
Cipher.ENCRYPT_MODE, iv, encryptKey, privateKeyBytes);
//生成mac
byte[] mac = generateMac(derivedKey, cipherText);
return createWalletFile(ecKeyPair, cipherText, iv, salt, mac, n, p);
}
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));
////AES加密相关参数,以及经过AES加密的密码
WalletFile.Crypto crypto = new WalletFile.Crypto();
crypto.setCipher(CIPHER);
crypto.setCiphertext(Numeric.toHexStringNoPrefix(cipherText));
walletFile.setCrypto(crypto);
//AES加密时的16字节初始化向量
WalletFile.CipherParams cipherParams = new WalletFile.CipherParams();
cipherParams.setIv(Numeric.toHexStringNoPrefix(iv));
crypto.setCipherparams(cipherParams);
//scrypt加密的相关参数,此加密算法被用来进行‘密码’加密。
crypto.setKdf(SCRYPT);
WalletFile.ScryptKdfParams kdfParams = new WalletFile.ScryptKdfParams();
kdfParams.setDklen(DKLEN);
kdfParams.setN(n);
kdfParams.setP(p);
kdfParams.setR(R);
kdfParams.setSalt(Numeric.toHexStringNoPrefix(salt));
crypto.setKdfparams(kdfParams);
//生成mac以及uuid,用来进行keystore信息验证
crypto.setMac(Numeric.toHexStringNoPrefix(mac));
walletFile.setCrypto(crypto);
walletFile.setId(UUID.randomUUID().toString());
walletFile.setVersion(CURRENT_VERSION);
return walletFile;
}
到此,钱包生成,下面是生成的keystore文件内容
{
"address": "445cb5c43d9bc8eb6c6e8297452f845c2facdee9",//钱包地址
"id": "857b3356-9d9d-455c-ae10-980214f95aa2",//UUID
"version": 3,//钱包版本
"crypto": {//AES加密,被用来加密私钥
"cipher": "aes-128-ctr", //AES加密用到的算法是aes-128-ctr
"cipherparams": {
"iv": "fc14dee2d8188e3545df3cc210fc7264"//AES加密时的16字节初始化向量,也就是32字符
},
"ciphertext": "5d52d969412224c07255c940ec1258f5baae5255b4b19a5fe587135225492c71",//私钥,根据'密码'(此密码已经进行了kdf加密)进行AES加密
"kdf": "scrypt",//PBKDF2加密用到的算法scrypt,此加密被用来进行‘密码’加密。
"kdfparams": {//第一层加密‘密码’时的salty以及n、p、R等参数
"dklen": 32,
"n": 262144,
"p": 1,
"r": 8,
"salt": "6d1df0eeca58c086d34dd1a13d08f796919479e78bc38f2043f05ceb3395b1c8"
},
"mac": "6e4de06fd9468ed04592486cbf6150ed2155610512ccdf5d6b2e5ace97cab4ba" //将ciphertext和‘密码’再次进行加密的产物,用于验证信息。
}
}
钱包创建相关代码(具体见Github内WalletGenService.kt)
class WalletGenService : IntentService("WalletGen Service") {
private var builder: NotificationCompat.Builder? = null
internal val mNotificationId = 152
private var normalMode = true
override fun onHandleIntent(intent: Intent?) {
val password = intent!!.getStringExtra("PASSWORD")
var privatekey = ""
if (intent.hasExtra("PRIVATE_KEY")) {
normalMode = false
privatekey = intent.getStringExtra("PRIVATE_KEY")
}
sendNotification()
try {
val walletAddress: String
if (normalMode) { // Create new key
walletAddress = OwnWalletUtils.generateNewWalletFile(password, File(this.filesDir, ""), true)
} else { // Private key passed
val keys = ECKeyPair.create(Hex.decode(privatekey))
walletAddress = OwnWalletUtils.generateWalletFile(password, keys, File(this.filesDir, ""), true)
}
WalletStorage.getInstance(this).add(FullWallet("0x" + walletAddress, walletAddress), this)
AddressNameConverter.getInstance(this).put("0x" + walletAddress, "Wallet " + ("0x" + walletAddress).substring(0, 6), this)
Settings.walletBeingGenerated = false
finished("0x" + walletAddress)
} catch (e: CipherException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
} catch (e: InvalidAlgorithmParameterException) {
e.printStackTrace()
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
} catch (e: NoSuchProviderException) {
e.printStackTrace()
}
}
private fun sendNotification() {
builder = NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.ic_notification)
.setColor(0x2d435c)
.setTicker(if (normalMode) getString(R.string.notification_wallgen_title) else getString(R.string.notification_wallimp_title))
.setContentTitle(this.resources.getString(if (normalMode) R.string.wallet_gen_service_title else R.string.wallet_gen_service_title_import))
.setOngoing(true)
.setProgress(0, 0, true)
.setContentText(getString(R.string.notification_wallgen_maytake))
val mNotifyMgr = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
mNotifyMgr.notify(mNotificationId, builder!!.build())
}
private fun finished(address: String) {
builder!!
.setContentTitle(if (normalMode) getString(R.string.notification_wallgen_finished) else getString(R.string.notification_wallimp_finished))
.setLargeIcon(Blockies.createIcon(address.toLowerCase()))
.setAutoCancel(true)
.setLights(Color.CYAN, 3000, 3000)
.setSound(android.provider.Settings.System.DEFAULT_NOTIFICATION_URI)
.setProgress(100, 100, false)
.setOngoing(false)
.setAutoCancel(true)
.setContentText(getString(R.string.notification_click_to_view))
if (android.os.Build.VERSION.SDK_INT >= 18)
// Android bug in 4.2, just disable it for everyone then...
builder!!.setVibrate(longArrayOf(1000, 1000))
val main = Intent(this, MainActivity::class.java)
main.putExtra("STARTAT", 1)
val contentIntent = PendingIntent.getActivity(this, 0,
main, PendingIntent.FLAG_UPDATE_CURRENT)
builder!!.setContentIntent(contentIntent)
val mNotifyMgr = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
mNotifyMgr.notify(mNotificationId, builder!!.build())
}
}
下一篇文章我会讲关于以太坊(Ethereum)转账的原理以及实现