前段時間,項目需求基于Stellar公鏈發(fā)行的衍生鏈,需求iOS安卓端開發(fā)DAPP錢包配合公鏈run,項目基本屬于破冰,國內(nèi)完全沒有任何資料去參考,一路走來都是新技術(shù)點,全憑google和自己慢慢填坑,完全從0開始,現(xiàn)在項目基本成熟已經(jīng)進入測試階段,所以有空余時間寫篇文章為其他需要玩恒星公鏈的攻城獅提點建議,講講坑.
Stellar API 傳送門: https://www.stellar.org/developers/reference/
Stellar Swift Sdk 傳送門: https://github.com/Soneso/stellar-ios-mac-sdk
首先,項目采用oc,swift混編,嵌入了部分C語言和C++,
整體布局由于去中心化APP的特異性,基本都是采用回調(diào)方式創(chuàng)建事件肾砂、構(gòu)造Operation和Trancations,
采用的隨機方式從.English單詞表生成的12位助記詞,通過bip39共識算法生成隨機公私鑰,
1 -- 創(chuàng)建賬戶
根據(jù)單詞表隨機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組),每個組編碼對應(yīng)一個0-2047的數(shù)字,該數(shù)字作為一個索引到wordlist,對應(yīng)獲得wordlist上相應(yīng)索引的值.
(4)最后,我們將這些數(shù)字轉(zhuǎn)換成單詞,最終合在一起作為助記句.
助記詞必須以32位的倍數(shù)選擇熵值entropy.隨著熵值的增加,句子長度增加,安全性提高.我們將初始熵長度稱為ENT,ENT的允許大小是128-256位,目前我采用的是bip39的256位算法.
為了從助記符創(chuàng)建二進制種子,我們使用PBKDF2函數(shù)(密鑰拉伸(Key stretching)函數(shù)),使用助記詞(UTF-8 NFKD)作為密碼,使用字符串"助記詞"+密碼(UTF-8 NFKD)作為salt.迭代計數(shù)設(shè)置為2048(即重復(fù)運算2048次).使用hma - sha512作為偽隨機函數(shù).派生鍵的長度是512位(= 64字節(jié),即最后的seed的長度).
因為這里考慮到以后錢包要和ETH,BTC等錢包攀上關(guān)系,所以從開始就已經(jīng)著手準備HD協(xié)議:
這個seed之后將被bip32或相似的方法使用來生成hd wallet,將助記句轉(zhuǎn)換為二進制種子句與生成句子完全無關(guān).這導(dǎo)致了相當簡單的代碼;句子結(jié)構(gòu)沒有限制,客戶機可以自由地實現(xiàn)自己的單詞列表,甚至可以實現(xiàn)整個句子生成器,這允許在單詞列表中靈活地進行類型檢測或其他目的.
雖然使用不是由“生成助記符”部分中描述的算法生成的助記符是可能的,但不建議這樣做宏悦,軟件必須使用wordlist計算助記符句子的校驗和镐确,并在其無效時發(fā)出警告.所描述的方法還提供了可信的可否認性,因為每個密碼都生成一個有效的種子(從而產(chǎn)生一個hd wallet),但是只有正確的一個才能使所需的錢包可用.
// MARK: - 根據(jù)12詞助記詞,導(dǎo)入賬戶
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 -- 查詢賬戶
這里不做過多的描述,因為準備大篇幅的內(nèi)容留在之后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!)") //其他衍生發(fā)行幣
}
}
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 -- 轉(zhuǎn)賬操作
這里就要詳細講一下Operations for Transaction,因為坑是真的很多,而且國內(nèi)也沒有像樣子的詳細介紹說明,由于去中心化的關(guān)系,基本一些邏輯上的操作全部要最小公鏈節(jié)點(DApp)來操作,這就造成了基本一個trancation當中必然要包含多個動作.
我們就以轉(zhuǎn)賬這個操作來說,需要有不低于3個步驟:
(1)確認sourceAccount源賬戶中,余額是否充足,拿到sourceAccountKeyPair用以在接下來創(chuàng)建paymentOperation,以確保我們有當前序列號,
(2)查詢destinationAccount目標賬戶是否激活開戶(因為由于節(jié)點數(shù)據(jù)庫的特異性,不可能鏈上全部賬戶全部存入Horizon數(shù)據(jù)庫),未開戶激活的賬戶,公鏈只默認存在于最小非共識節(jié)點(DApp終端),
(3)通過轉(zhuǎn)賬幣種那種當前幣的Asset,一般本幣為native,其他衍生發(fā)行幣是ASSET_TYPE_CREDIT_ALPHANUM4以下發(fā)行的
通過ALPHANUM4衍生發(fā)行幣拿到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的發(fā)行人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的發(fā)行人Issue公鑰地址")
coinTypeAsset = Asset(type: AssetType.ASSET_TYPE_CREDIT_ALPHANUM4, code: coinName, issuer: hourIssuerKeyPair)
}
catch {
// 錯誤
}
}
return coinTypeAsset!
}
轉(zhuǎn)賬Operation:
// MARK: - 轉(zhuǎn)賬
@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)
/* 獲取帳戶數(shù)據(jù),以確保我們有當前序列號 */
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))
/* 構(gòu)建包含我們支付操作的事務(wù)(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 {
//交易過程中,數(shù)據(jù)錯誤
}
case .failure(let error):
StellarSDKLog.printHorizonRequestErrorMessage(tag:"func-transactions-查詢", horizonRequestError:error)
}
}
}
catch {
if (self.stellarErrorBlock != nil) {
self.stellarErrorBlock!("格式錯誤")
}
}
}
以上轉(zhuǎn)賬Operation有幾個小細節(jié)處,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)
這里的createOperation有一些簡單說明,激活才做需要sourceAccount為當前已經(jīng)激活的account,而且必須最少有公鏈設(shè)置的賬戶創(chuàng)建最低流明作為初始幣持有(如果低于初始幣持有,會扣除手續(xù)費,并且賬戶激活失敗),很坑...
// 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)
/* 獲取帳戶數(shù)據(jù),以確保我們有當前序列號 */
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流明
/* 構(gòu)建包含我們支付操作的事務(wù)(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 {
//交易過程中,數(shù)據(jù)錯誤
}
case .failure(let error):
StellarSDKLog.printHorizonRequestErrorMessage(tag:"func-transactions-查詢", horizonRequestError:error)
}
}
}
catch {
//格式錯誤
}
}
5 -- 建立信任線
建立信任線的操作,基本跟轉(zhuǎn)賬Operation中差距不大,只是在打包XDR信封的時候,需要裝入信封的Operation轉(zhuǎn)變?yōu)閏hangeTrustOperation,其余包括Asset對象創(chuàng)建都是同理.
當中有一點需要注意
changeTrustOperation創(chuàng)建的時候要確認,當前幣中在token地址上是否有余額,如果有余額會報錯horizon信任線失敗,只有在全部轉(zhuǎn)出余額為0的時候才能轉(zhuǎn)變信任線為NO,并且當你想到轉(zhuǎn)換信任線為YES的時候,需要資產(chǎn)發(fā)行人Issee,并且需要一個已經(jīng)持有該幣種的最小子節(jié)點(DApp終端)給與你最低流明,并開啟信任操作.
// MARK: - "1"->建立信任, "0"->取消信任
@objc func changeTrustTimeHour(mySecretSeed:String, coinName: String, trust:String) -> Void {
/* 源帳戶,自己的帳戶 */
let sourceAccountKeyPair = try! KeyPair(secretSeed:mySecretSeed)
/* 獲取帳戶數(shù)據(jù),以確保我們有當前序列號 */
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))
/* 構(gòu)建包含我們支付操作的事務(wù)(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 {
//響應(yīng)操作
}
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并不完全一樣(雖然在實際應(yīng)用中二者可以互換),因為Rijndael加密法可以支持更大范圍的區(qū)塊和密鑰長度:AES的區(qū)塊長度固定為128位,密鑰長度則可以是128,192或256位;而Rijndael使用的密鑰和區(qū)塊長度可以是32位的整數(shù)倍,以128位為下限,256位為上限.加密過程中使用的密鑰是由Rijndael密鑰生成方案產(chǎn)生.
大多數(shù)AES計算是在一個特別的有限域完成的.
不帶模式和填充來獲取AES算法的時候,其默認使用AES/ECB/PKCS5Padding(輸入可以不是16字節(jié),也不需要填充向量).
這里有一個巨坑!!:
安卓和ios一同開發(fā)的攻城獅們注意了,這里的AES算法涉及到偏移位padding5和padding7的區(qū)別時候,肯定會讓你們束手無策,這里有一個矛盾點就是ios的系統(tǒng)庫<CommonCrypto/CommonDigest.h>僅僅支持AES256的padding5算法,而安卓的系統(tǒng)庫僅僅支持AES256的padding7算法,所以就會產(chǎn)生一個最大的矛盾點,如果按照各自平臺的偏移位去加解密,那最后的結(jié)果會導(dǎo)致在各自平臺內(nèi)完全可以加解密成功,但是如果跨平臺的話就是因為偏移位問題出現(xiàn),加解密位數(shù)報錯或者加解密直接失敗,這里最后找到的解決辦法是采用KDF算法,密碼偏移輪詢
CCKeyDerivationPBKDF(kCCPBKDF2, // algorithm算法
password.UTF8String, // password密碼
password.length, // passwordLength密碼的長度
salt.bytes, // salt內(nèi)容
salt.length, // saltLen長度
kCCPRFHmacAlgSHA1, // PRF
10000, // rounds循環(huán)次數(shù)
derivedKey.mutableBytes, // derivedKey
derivedKey.length); // derivedKeyLen derive:出自
并且需要設(shè)置一個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 結(jié)果:%@", 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 結(jié)果:%@", pasword, User.aes256_iv, decStr);
return decStr;
}
AES256算法核心
/**
* AES加解密算法
* @param operation kCCEncrypt(加密)kCCDecrypt(解密)
* @param data 待操作Data數(shù)據(jù)
* @param key key
* @param iv 向量
*/
- (NSData *)AES128operation:(CCOperation)operation data:(NSData *)data key:(NSString *)key iv:(NSString *)iv {
char keyPtr[kCCKeySizeAES256 + 1]; //kCCKeySizeAES128是加密位數(shù) 可以替換成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;
// 設(shè)置加密參數(shù)
/** 這里設(shè)置的參數(shù)ios默認為CBC加密方式饼煞,如果需要其他加密方式如ECB源葫,在kCCOptionPKCS7Padding這個參數(shù)后邊加上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;
}