基于Stellar公链iOSDApp
前段时间,项目需求基于Stellar公链发行的衍生链,需求iOS安卓端开发DAPP钱包配合公链run,项目基本属于破冰,国内完全没有任何资料去参考,一路走来都是新技术点,全凭google和自己慢慢填坑,完全从0开始,现在项目基本成熟已经进入测试阶段,所以有空余时间写篇文章为其他需要玩恒星公链的攻城狮提点建议,讲讲坑.
Stellar API 传送门: https://www.stellar.org/developers/reference/
Stellar Swift Sdk 传送门: https://github.com/Soneso/stellar-ios-mac-sdk
首先,项目采用oc,swift混编,嵌入了部分C语言和C++,
整体布局由于去中心化APP的特异性,基本都是采用回调方式创建事件、构造Operation和Trancations,
采用的随机方式从.English单词表生成的12位助记词,通过bip39共识算法生成随机公私钥,
1 -- 创建账户
根据单词表随机12位 Mnemonic助记词
// MARK: - 初始化sdk
let sdk = StellarSDK.init(withHorizonUrl: "*****") //由于工作原因这里的Horizon暂时不能公开,为公链的Horizon地址
let mnemonic = Wallet.generate12WordMnemonic()
BIP39
与处理钱包seed的原始二进制或十六进制表示形式相比,助记码或句子更适合于人类交互.这个句子可以写在纸上,也可以通过电话告诉对方.
(1)首先,生成ENT比特的初始熵entropy(如下面的例子00000000000000000000000000000000,16进制,熵长度为32*4=128).
(2)通过对初始熵entropy取SHA256散列来获得CS位(CS= 熵长度/32=4,取得到的SHA256散列的前CS位)校验和,然后将校验和附加到初始熵的末尾.
(3)接下来,(熵entropy+校验和)被分成以11位为一组(一共MS组),每个组编码对应一个0-2047的数字,该数字作为一个索引到wordlist,对应获得wordlist上相应索引的值.
(4)最后,我们将这些数字转换成单词,最终合在一起作为助记句.
助记词必须以32位的倍数选择熵值entropy.随着熵值的增加,句子长度增加,安全性提高.我们将初始熵长度称为ENT,ENT的允许大小是128-256位,目前我采用的是bip39的256位算法.
为了从助记符创建二进制种子,我们使用PBKDF2函数(密钥拉伸(Key stretching)函数),使用助记词(UTF-8 NFKD)作为密码,使用字符串"助记词"+密码(UTF-8 NFKD)作为salt.迭代计数设置为2048(即重复运算2048次).使用hma - sha512作为伪随机函数.派生键的长度是512位(= 64字节,即最后的seed的长度).
因为这里考虑到以后钱包要和ETH,BTC等钱包攀上关系,所以从开始就已经着手准备HD协议:
这个seed之后将被bip32或相似的方法使用来生成hd wallet,将助记句转换为二进制种子句与生成句子完全无关.这导致了相当简单的代码;句子结构没有限制,客户机可以自由地实现自己的单词列表,甚至可以实现整个句子生成器,这允许在单词列表中灵活地进行类型检测或其他目的.
虽然使用不是由“生成助记符”部分中描述的算法生成的助记符是可能的,但不建议这样做,软件必须使用wordlist计算助记符句子的校验和,并在其无效时发出警告.所描述的方法还提供了可信的可否认性,因为每个密码都生成一个有效的种子(从而产生一个hd wallet),但是只有正确的一个才能使所需的钱包可用.
// MARK: - 根据12词助记词,导入账户
let bip39SeedData = Mnemonic.createSeed(mnemonic: mnemonic)
let masterPrivateKey = Ed25519Derivation(seed: bip39SeedData)
let purpose = masterPrivateKey.derived(at: 44) //purpose,coinType,account为3次算法外位偏移量
let coinType = purpose.derived(at: 358)
let account = coinType.derived(at: 0)
let keyPair = try! KeyPair.init(seed: Seed(bytes: account.raw.bytes))
print("key pair - accountId: \(keyPair.accountId)")
print("key pair - secretSeed: \(keyPair.secretSeed!)")
2 -- 查询账户
这里不做过多的描述,因为准备大篇幅的内容留在之后Trancation和XDR信封签名的过程,所以直接展示封装核心代码
// MARK: - 查询账户
sdk.accounts.getAccountDetails(accountId: keyPair.accountId) { (response) -> (Void) in
switch response {
case .success(let accountDetails):
for balance in accountDetails.balances {
switch balance.assetType {
case AssetTypeAsString.NATIVE:
print("balance: \(balance.balance) XLM") //native币余额
default:
print("balance: \(balance.balance) \(balance.assetCode!) issuer: \(balance.assetIssuer!)") //其他衍生发行币
}
}
for signer in accountDetails.signers {
print("signer public key: \(signer.publicKey)")
}
print("sequence number: \(accountDetails.sequenceNumber)")
print("auth required: \(accountDetails.flags.authRequired)")
print("auth revocable: \(accountDetails.flags.authRevocable)")
for (key, value) in accountDetails.data {
print("data key: \(key) value: \(value.base64Decoded() ?? "")")
}
case .failure(let error):
print(error.localizedDescription)
}
}
3 -- 转账操作
这里就要详细讲一下Operations for Transaction,因为坑是真的很多,而且国内也没有像样子的详细介绍说明,由于去中心化的关系,基本一些逻辑上的操作全部要最小公链节点(DApp)来操作,这就造成了基本一个trancation当中必然要包含多个动作.
我们就以转账这个操作来说,需要有不低于3个步骤:
(1)确认sourceAccount源账户中,余额是否充足,拿到sourceAccountKeyPair用以在接下来创建paymentOperation,以确保我们有当前序列号,
(2)查询destinationAccount目标账户是否激活开户(因为由于节点数据库的特异性,不可能链上全部账户全部存入Horizon数据库),未开户激活的账户,公链只默认存在于最小非共识节点(DApp终端),
(3)通过转账币种那种当前币的Asset,一般本币为native,其他衍生发行币是ASSET_TYPE_CREDIT_ALPHANUM4以下发行的
通过ALPHANUM4衍生发行币拿到Asset对象的简单过程:
// MARK: 通过coin名字拿到 asset
func getCoinNameAsset(coinName:String) -> Asset {
var coinTypeAsset = Asset(type: AssetType.ASSET_TYPE_NATIVE)
if coinName == "coin1" {
do {
let timeIssuerKeyPair = try KeyPair(accountId: "币1的发行人Issue公钥地址")
coinTypeAsset = Asset(type: AssetType.ASSET_TYPE_CREDIT_ALPHANUM4, code: coinName, issuer: timeIssuerKeyPair)
}
catch {
// 错误
}
}
else if coinName == "coin2" {
do {
let hourIssuerKeyPair = try KeyPair(accountId: "币2的发行人Issue公钥地址")
coinTypeAsset = Asset(type: AssetType.ASSET_TYPE_CREDIT_ALPHANUM4, code: coinName, issuer: hourIssuerKeyPair)
}
catch {
// 错误
}
}
return coinTypeAsset!
}
接下来走入转账Operation:
// MARK: - 转账
@objc func transactions(mySecretSeed: String, toAccountId: String, coinAmount: NSInteger, memoText: String, coinName: String) -> Void {
/* 源帐户,自己的帐户 */
let sourceAccountKeyPair = try! KeyPair(secretSeed:mySecretSeed)
do {
/* 目标帐户 */
let destinationAccountKeyPair = try KeyPair(accountId: toAccountId)
/* 获取帐户数据,以确保我们有当前序列号 */
sdk.accounts.getAccountDetails(accountId: sourceAccountKeyPair.accountId) { (response) -> (Void) in
switch response {
case .success(let accountResponse):
do {
/* 建立支付操作 */
let paymentOperation = PaymentOperation(destination: destinationAccountKeyPair,
asset: self.getCoinNameAsset(coinName: coinName),
amount: Decimal(coinAmount))
/* 构建包含我们支付操作的事务(transaction) */
let transaction = try Transaction(sourceAccount: accountResponse,
operations: [paymentOperation],
memo: Memo.text(memoText),
timeBounds:nil)
/* 用秘钥给transaction签名 */
try transaction.sign(keyPair: sourceAccountKeyPair, network: Network.public)
/* 提交transaction */
try self.sdk.transactions.submitTransaction(transaction: transaction) { (response) -> (Void) in
switch response {
case .success(_):
//success
case .failure(let error):
StellarSDKLog.printHorizonRequestErrorMessage(tag:"func-transactions-交易", horizonRequestError:error)
}
}
} catch {
//交易过程中,数据错误
}
case .failure(let error):
StellarSDKLog.printHorizonRequestErrorMessage(tag:"func-transactions-查询", horizonRequestError:error)
}
}
}
catch {
if (self.stellarErrorBlock != nil) {
self.stellarErrorBlock!("格式错误")
}
}
}
以上转账Operation有几个小细节处,Network.public为当前公链horizon地址的publicNet,如果这里还是使用原始SDK中的测试net,会根本run不通公链horizon:
/* 用秘钥给transaction签名 */
try transaction.sign(keyPair: sourceAccountKeyPair, network: Network.public)
---------------------------------------------------------------------------------------
// Network.swift
// stellarsdk
public enum Network: String {
case `public` = "your public network"
case testnet = "Test SDF Network ; September 2015"
var networkId: Data {
get {
return self.rawValue.sha256Hash
}
}
}
4 -- 新账户激活(createOperation)
// MARK: - 给新账户,激活账户(不低于100流明)
@objc func createActiviteAccount(mySecretSeed: String, toAccountId: String, coinAmount: NSInteger, memoText: String) -> Void {
/* 源帐户,自己的帐户 */
let sourceAccountKeyPair = try! KeyPair(secretSeed:mySecretSeed)
do {
/* 目标帐户 */
let destinationAccountKeyPair = try KeyPair(accountId: toAccountId)
/* 获取帐户数据,以确保我们有当前序列号 */
sdk.accounts.getAccountDetails(accountId: sourceAccountKeyPair.accountId) { (response) -> (Void) in
switch response {
case .success(let accountResponse):
do {
/* 建立激活操作 */
let createOpention = CreateAccountOperation(destination: destinationAccountKeyPair, startBalance: Decimal(coinAmount))//不低于100流明
/* 构建包含我们支付操作的事务(transaction) */
let transaction = try Transaction(sourceAccount: accountResponse,
operations: [createOpention],
memo: Memo.text(memoText),
timeBounds:nil)
/* 用秘钥给transaction签名 */
try transaction.sign(keyPair: sourceAccountKeyPair, network: Network.public)
/* 提交transaction */
try self.sdk.transactions.submitTransaction(transaction: transaction) { (response) -> (Void) in
switch response {
case .success(_):
//success
case .failure(let error):
StellarSDKLog.printHorizonRequestErrorMessage(tag:"func-transactions-交易", horizonRequestError:error)
}
}
} catch {
//交易过程中,数据错误
}
case .failure(let error):
StellarSDKLog.printHorizonRequestErrorMessage(tag:"func-transactions-查询", horizonRequestError:error)
}
}
}
catch {
//格式错误
}
}
5 -- 建立信任线
建立信任线的操作,基本跟转账Operation中差距不大,只是在打包XDR信封的时候,需要装入信封的Operation转变为changeTrustOperation,其余包括Asset对象创建都是同理.
当中有一点需要注意
changeTrustOperation创建的时候要确认,当前币中在token地址上是否有余额,如果有余额会报错horizon信任线失败,只有在全部转出余额为0的时候才能转变信任线为NO,并且当你想到转换信任线为YES的时候,需要资产发行人Issee,并且需要一个已经持有该币种的最小子节点(DApp终端)给与你最低流明,并开启信任操作.
// MARK: - "1"->建立信任, "0"->取消信任
@objc func changeTrustTimeHour(mySecretSeed:String, coinName: String, trust:String) -> Void {
/* 源帐户,自己的帐户 */
let sourceAccountKeyPair = try! KeyPair(secretSeed:mySecretSeed)
/* 获取帐户数据,以确保我们有当前序列号 */
sdk.accounts.getAccountDetails(accountId: sourceAccountKeyPair.accountId) { (response) -> (Void) in
switch response {
case .success(let accountResponse):
do {
//Decimal()->建立信任, Decimal(0)->取消信任
let changeTrustTimeHourOperation = ChangeTrustOperation(asset: self.getCoinNameAsset(coinName: coinName), limit: (trust == "0" ? 0 : 100000000))
/* 构建包含我们支付操作的事务(transaction) */
let transaction = try Transaction(sourceAccount: accountResponse,
operations: [changeTrustTimeHourOperation],
memo: Memo.none,
timeBounds:nil)
/* 用秘钥给transaction签名 */
try transaction.sign(keyPair: sourceAccountKeyPair, network: Network.public)
/* 提交transaction */
try self.sdk.transactions.submitTransaction(transaction: transaction) { (response) -> (Void) in
switch response {
case .success(_):
print((trust == "1" ? "信任" : "取消信任") + "success")
case .failure(let error):
StellarSDKLog.printHorizonRequestErrorMessage(tag:"func-transactions-交易", horizonRequestError:error)
}
}
}
catch {
// 信任错误
}
case .failure(let error):
StellarSDKLog.printHorizonRequestErrorMessage(tag:"func-transactions-查询", horizonRequestError:error)
}
}
}
6 -- 查询交易记录
// MARK: - 查询交易记录
@objc func requestPaymentsRecord(accountId:String, limit:Int) -> Void {
sdk.payments.getPayments(forAccount: accountId, order:Order.ascending, limit:limit) { response in
switch response {
case .success(let paymentsResponse):
for payment in paymentsResponse.records {
//响应操作
}
case .failure(let error):
print(error.localizedDescription)
}
}
}
7 -- SHA256加密算法
#import <CommonCrypto/CommonDigest.h>
- (NSString *)SHA256 {
const char *s = [self cStringUsingEncoding:NSASCIIStringEncoding];
NSData *keyData = [NSData dataWithBytes:s length:strlen(s)];
uint8_t digest[CC_SHA256_DIGEST_LENGTH] = {0};
CC_SHA256(keyData.bytes, (CC_LONG)keyData.length, digest);
NSData *out = [NSData dataWithBytes:digest length:CC_SHA256_DIGEST_LENGTH];
NSString *hash = [out description];
hash = [hash stringByReplacingOccurrencesOfString:@" " withString:@""];
hash = [hash stringByReplacingOccurrencesOfString:@"<" withString:@""];
hash = [hash stringByReplacingOccurrencesOfString:@">" withString:@""];
return hash;
}
8 -- 划重点!!! AES256算法!
严格地说,AES和Rijndae并不完全一样(虽然在实际应用中二者可以互换),因为Rijndael加密法可以支持更大范围的区块和密钥长度:AES的区块长度固定为128位,密钥长度则可以是128,192或256位;而Rijndael使用的密钥和区块长度可以是32位的整数倍,以128位为下限,256位为上限.加密过程中使用的密钥是由Rijndael密钥生成方案产生.
大多数AES计算是在一个特别的有限域完成的.
不带模式和填充来获取AES算法的时候,其默认使用AES/ECB/PKCS5Padding(输入可以不是16字节,也不需要填充向量).
这里有一个巨坑!!:
安卓和ios一同开发的攻城狮们注意了,这里的AES算法涉及到偏移位padding5和padding7的区别时候,肯定会让你们束手无策,这里有一个矛盾点就是ios的系统库<CommonCrypto/CommonDigest.h>仅仅支持AES256的padding5算法,而安卓的系统库仅仅支持AES256的padding7算法,所以就会产生一个最大的矛盾点,如果按照各自平台的偏移位去加解密,那最后的结果会导致在各自平台内完全可以加解密成功,但是如果跨平台的话就是因为偏移位问题出现,加解密位数报错或者加解密直接失败,这里最后找到的解决办法是采用KDF算法,密码偏移轮询
CCKeyDerivationPBKDF(kCCPBKDF2, // algorithm算法
password.UTF8String, // password密码
password.length, // passwordLength密码的长度
salt.bytes, // salt内容
salt.length, // saltLen长度
kCCPRFHmacAlgSHA1, // PRF
10000, // rounds循环次数
derivedKey.mutableBytes, // derivedKey
derivedKey.length); // derivedKeyLen derive:出自
并且需要设置一个buff密码偏移
// 密码偏移
static Byte saltBuff[] = {0,1,2,3,4,5,6,7,8,9,0xA,0xB,0xC,0xD,0xE,0xF};
接下来说下aes加解密
// AES256加密
- (NSString *)aes256_encryptWithPassword:(NSString *)pasword aesIV:(NSString *)iv
{
NSData *data = [self dataUsingEncoding:NSUTF8StringEncoding];
NSData *AESData = [self AES128operation:kCCEncrypt
data:data
key:pasword
iv:iv];
NSString *baseStr_GTM = [self encodeBase64Data:AESData];
NSLog(@"加密 \n 密码:%@ \n iv:%@ \n 结果:%@", pasword, User.aes256_iv, baseStr_GTM);
return baseStr_GTM;
}
// AES256解密
- (NSString *)aes256_decryptWithPassword:(NSString *)pasword
{
NSData *baseData = [[NSData alloc]initWithBase64EncodedString:self options:0];
NSData *AESData = [self AES128operation:kCCDecrypt
data:baseData
key:pasword
iv:User.aes256_iv];
NSString *decStr = [[NSString alloc] initWithData:AESData encoding:NSUTF8StringEncoding];
NSLog(@"解密 \n 密码:%@ \n iv:%@ \n 结果:%@", pasword, User.aes256_iv, decStr);
return decStr;
}
AES256算法核心
/**
* AES加解密算法
* @param operation kCCEncrypt(加密)kCCDecrypt(解密)
* @param data 待操作Data数据
* @param key key
* @param iv 向量
*/
- (NSData *)AES128operation:(CCOperation)operation data:(NSData *)data key:(NSString *)key iv:(NSString *)iv {
char keyPtr[kCCKeySizeAES256 + 1]; //kCCKeySizeAES128是加密位数 可以替换成256位的
bzero(keyPtr, sizeof(keyPtr));
// IV
char ivPtr[kCCBlockSizeAES128 + 1];
bzero(ivPtr, sizeof(ivPtr));
[iv getCString:ivPtr maxLength:sizeof(ivPtr) encoding:NSUTF8StringEncoding];
size_t bufferSize = [data length] + kCCBlockSizeAES128;
void *buffer = malloc(bufferSize);
size_t numBytesEncrypted = 0;
// 设置加密参数
/** 这里设置的参数ios默认为CBC加密方式,如果需要其他加密方式如ECB,在kCCOptionPKCS7Padding这个参数后边加上kCCOptionECBMode,即kCCOptionPKCS7Padding | kCCOptionECBMode,但是记得修改上边的偏移量,因为只有CBC模式有偏移量之说 */
CCCryptorStatus cryptorStatus = CCCrypt(operation,
kCCAlgorithmAES128,
kCCOptionPKCS7Padding,
[[NSString AESKeyForPassword:key] bytes],
kCCKeySizeAES256,
ivPtr,
[data bytes],
[data length],
buffer,
bufferSize,
&numBytesEncrypted);
if(cryptorStatus == kCCSuccess) {
NSLog(@"Success");
return [NSData dataWithBytesNoCopy:buffer length:numBytesEncrypted];
} else {
NSLog(@"Error");
}
free(buffer);
return nil;
}