混社區的時候(QQ羣)老是聽到大佬們聊到nep,好奇心驅使下就去neo官網找資料,然鵝,什麼都沒找到。後來就請教大佬,才知道nep是neo一系列提案,文檔並不在neo官網,在這裏。可是很奇怪的是我到目前爲止只據說到了nep2,nep5和nep6,其他的幾個提案彷佛沒什麼人講,之後有機會我再仔細瞭解下。nep2提案是一套加密私鑰的算法,nep5提案是發佈token相關的,nep6則是定義了標準化的neo錢包數據結構。因爲我如今瞭解的最詳盡的是nep2和nep6(好幾個sdk源碼都擼了一遍),並且nep2和nep6也是相輔相成密不可分,因此這裏我就先主要從源碼角度分析下nep2和nep6. 注: 本文行文邏輯 新帳戶 => nep2加解密 => 添加到nep6錢包node
和幾乎全部的加密貨幣同樣,NEO的帳戶也是用了基於橢圓曲線的公私鑰生成算法,在NEO的帳戶體系中,公鑰由私鑰計算而來,地址又由公鑰計算而來,能夠說只要掌握了私鑰,就徹底掌握了這個帳戶。數學原理請移步這裏下載密碼學書籍學習。 NEO的私鑰是隨機生成的長度爲32的字節數組:git
源碼位置:neo/Wallets/Wallet.cs/CreateAccount()github
byte[] privateKey = new byte[32];
using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
{
rng.GetBytes(privateKey);
}
複製代碼
因爲各個節點新帳戶的生成徹底在本地進行,因此必須保證隨機數生成器徹底隨機也就是安全隨機才能真正確保帳戶的惟一性以及安全性,這裏我研究了不一樣平臺採起的安全隨機數策略,首先就是neo內核C#版本採用的RandomNumberGenerator類,這個隨機數生成算法以當前系統runtime環境參數做爲熵源產生隨機數,雖然執行效率比System.Random要慢上兩個數量級,可是產生的結果倒是安全的。算法
這裏我還想說一下我在開發NEO錢包小程序的時候遇到的問題,那就是微信小程序並不提供安全的隨機數生成算法,同時也不支持node內置的crypto,這讓我糾結了好久,由於沒有安全的隨機數生成算法,那麼這個錢包幾乎就是不可用的。我曾想過:json
等方法來做爲熵源,可是第一種密鑰空間過小,第二種沒辦法實現。後來我發如今每次獲取用戶受權數據的時候,會收到一段加密的字符串。我研究了下這個加密算法,主要是AES-128-CBC,並且每次解密初始向量都是不一樣的,長度也徹底知足需求,所以這段加密字符串能夠認爲是安全隨機。小程序
源碼位置:NewEconoLab/NeoWalletForWeChat/blob/master/src/utils/random.js微信小程序
export async function getSecureRandom(len) {
wepy.showLoading({ title: '獲取隨機數種子' });
let random = ''
const code = await this.getLoginCode();
const userinfo = await this.getUserInfo();
console.log(code)
random = SHA256(code + random).toString()
random = SHA256(userinfo.signature + random).toString()
random = SHA256(userinfo.encryptedData + random).toString()
random = SHA256(userinfo.iv + random).toString()
console.log(random)
wepy.hideLoading();
return random.slice(0, len)
}
複製代碼
NEO從私鑰計算公鑰的算法和比特幣是同樣的,這部分講的最好的固然是《Mastering BitCoin》中的第四章(下載鏈接),其中不只詳盡生動的講解了比特幣公私鑰生成原理,並且輔助了大量的插圖便於理解。比特幣在生成公鑰的時候選取的曲線是secp256k1曲線,而NEO選取的則是secp256r1。在StackOverflow上也有關於這兩個曲線哪一個更安全的討論,詳情點擊鏈接,可是這個不在個人討論範圍。下面是secp256r1定義:數組
源碼位置:neo/Cryptography/ECC/ECCurve.cs緩存
/// <summary>
/// 曲線secp256r1
/// </summary>
public static readonly ECCurve Secp256r1 = new ECCurve
(
BigInteger.Parse("00FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF", NumberStyles.AllowHexSpecifier),
BigInteger.Parse("00FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC", NumberStyles.AllowHexSpecifier),
BigInteger.Parse("005AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B", NumberStyles.AllowHexSpecifier),
BigInteger.Parse("00FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", NumberStyles.AllowHexSpecifier),
("04" + "6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296" + "4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5").HexToBytes()
);
複製代碼
以上源碼是NEO中secp256r1標準橢圓曲線的定義,哪怕不從密碼學角度來看,就這參數的長度就給人一種想狗帶的感受。 生成公鑰的時候,私鑰須要乘上一個預先定義在曲線上的基點,得到的結果就是公鑰。這個基點被稱爲G,全部的NEO節點的G都是相同的,也就是Secp256r1定義中最後那個特別長的字節數組。 《Mastering BitCoin》中的介紹以下:安全
_K = k * G
where k is the private key, G is the generator point, and K is the resulting public key, a point on the curve. Because the generator point is always the same for all bitcoin users, a private key k multiplied with G will always result in the same public key K. The relationship between k and K is fixed, but can only be calculated in one direction, from k to K. That’s why a bitcoin address (derived from K) can be shared with anyone and does not reveal the user’s private key (k)._
在NEO core中,這部分代碼在KeyPair類中,可是因爲計算部分主要是關於ECC的,因此我就不貼了。
前文已經說過neo的地址是由公鑰計算來的,可是其實還並不許確,這中間仍是有很複雜的過程的。首先根據私鑰生成帳戶的代碼在NEP6Wallet類中:
源碼位置:neo/Implementations/Wallets/NEP6/NEP6Wallet.cs
public override WalletAccount CreateAccount(byte[] privateKey)
{
KeyPair key = new KeyPair(privateKey); //根據私鑰生成公私鑰對
NEP6Contract contract = new NEP6Contract //生成合約
{
Script = Contract.CreateSignatureRedeemScript(key.PublicKey), //合約腳本
ParameterList = new[] { ContractParameterType.Signature },
ParameterNames = new[] { "signature" },
Deployed = false //不須要部署的鑑權合約
};
NEP6Account account = new NEP6Account(this, contract.ScriptHash, key, password)
{
Contract = contract
};
AddAccount(account, false);
return account;
}
複製代碼
從源碼中能夠看出,在生成新帳戶時,會根據公鑰建立一個鑑權合約,建立合約的代碼在Contract類的CreateSignatureRedeemScript方法中:
源碼位置:neo/SmartContract/Contract.cs
public static byte[] CreateSignatureRedeemScript(ECPoint publicKey)
{
using (ScriptBuilder sb = new ScriptBuilder())
{
sb.EmitPush(publicKey.EncodePoint(true));//push公鑰編碼後的字節數組
sb.Emit(OpCode.CHECKSIG);
return sb.ToArray();
}
}
複製代碼
這個方法會返回合約的腳本,地址就是根據這個腳本的哈希值得來的。在生成地址的時候,會傳入這個合約腳本的哈希值:
源碼位置:neo/Wallets/Wallet.cs
public static string ToAddress(UInt160 scriptHash)
{
byte[] data = new byte[21];
data[0] = Settings.Default.AddressVersion;
Buffer.BlockCopy(scriptHash.ToArray(), 0, data, 1, 20);
return data.Base58CheckEncode();
}
複製代碼
在生成地址的時候,首先申請21字節緩衝區,緩衝區首字節設置爲地址版本校驗位,後20字節copy自合約哈希的前20個字節,而後對這個緩衝區進行base58加密獲得的值就是咱們的地址。 總體流程和BieCoin對好比下:
第一張比較醜的流程圖是我畫的NEO地址生成過程,第二張是從《Mastering BitCoin》書中截取的比特幣地址生成流程,經過對比能夠看出,除了NEO的地址是根據合約腳本哈希值而BItCoin是Sha256+RIPEMD160以後的摘要生成以外,二者的地址計算過程幾乎一摸同樣。
上文中已經從私鑰到地址的整個流程都分析完了,若是是使用NEO帳戶的話,到上一小節,已經徹底夠了。從本小節日後講的都是關於帳戶安全和帳戶管理的部分。 nep2是爲了確保NEO帳戶私鑰安全而提出的私鑰加密提案,在提案裏詳細講解了加密和解密的參數以及流程規範。 nep2分爲兩個部分,一個是加密,另外一個是解密。加密的代碼以下:
源碼位置:neoWallets/KeyPair.cs
public string Export(string passphrase, int N = 16384, int r = 8, int p = 8)
{
using (Decrypt())
{
//獲取地址合約腳本哈希
UInt160 script_hash = Contract.CreateSignatureRedeemScript(PublicKey).ToScriptHash();
//獲取地址
string address = Wallet.ToAddress(script_hash);
//獲取地址摘要前四字節
byte[] addresshash = Encoding.ASCII.GetBytes(address).Sha256().Sha256().Take(4).ToArray();
//計算scrypt key
byte[] derivedkey = SCrypt.DeriveKey(Encoding.UTF8.GetBytes(passphrase), addresshash, N, r, p, 64);
byte[] derivedhalf1 = derivedkey.Take(32).ToArray();
byte[] derivedhalf2 = derivedkey.Skip(32).ToArray();
//aes加密
byte[] encryptedkey = XOR(PrivateKey, derivedhalf1).AES256Encrypt(derivedhalf2);
byte[] buffer = new byte[39];
//校驗位
buffer[0] = 0x01;
buffer[1] = 0x42;
buffer[2] = 0xe0;
//將地址摘要前四字節寫入緩存
Buffer.BlockCopy(addresshash, 0, buffer, 3, addresshash.Length);
//密文寫入緩存
Buffer.BlockCopy(encryptedkey, 0, buffer, 7, encryptedkey.Length);
//base58加密
return buffer.Base58CheckEncode();
}
}
複製代碼
這個算法就是徹底依據nep2提案的標準進行實現的,須要說明的是在最後的數據格式裏,前三字節是校驗位,以後四個字節是地址的哈希值,最後是密鑰的密文,之因此構造這樣的數據結構,是由於在解密的時候還須要從中提取地址哈希用於獲取scrypt key。加密流程圖以下:
而解密的過程則是和加密相反:
源碼位置:neo/Wallets/Wallet.cs
public static byte[] GetPrivateKeyFromNEP2(string nep2, string passphrase, int N = 16384, int r = 8, int p = 8)
{
if (nep2 == null) throw new ArgumentNullException(nameof(nep2));
if (passphrase == null) throw new ArgumentNullException(nameof(passphrase));
//base58解密
byte[] data = nep2.Base58CheckDecode();
//格式校驗
if (data.Length != 39 || data[0] != 0x01 || data[1] != 0x42 || data[2] != 0xe0)
throw new FormatException();
byte[] addresshash = new byte[4];
//讀取地址哈希
Buffer.BlockCopy(data, 3, addresshash, 0, 4);
//計算scrypt key 這裏結果和加密的 scrypt key須要相同
byte[] derivedkey = SCrypt.DeriveKey(Encoding.UTF8.GetBytes(passphrase), addresshash, N, r, p, 64);
byte[] derivedhalf1 = derivedkey.Take(32).ToArray();
byte[] derivedhalf2 = derivedkey.Skip(32).ToArray();
byte[] encryptedkey = new byte[32];
Buffer.BlockCopy(data, 7, encryptedkey, 0, 32);
//aes解密獲取私鑰
byte[] prikey = XOR(encryptedkey.AES256Decrypt(derivedhalf2), derivedhalf1);
//計算公鑰
Cryptography.ECC.ECPoint pubkey = Cryptography.ECC.ECCurve.Secp256r1.G * prikey;
//獲取帳戶合約腳本哈希
UInt160 script_hash = Contract.CreateSignatureRedeemScript(pubkey).ToScriptHash();
//計算地址
string address = ToAddress(script_hash);
//驗證解密結果
if (!Encoding.ASCII.GetBytes(address).Sha256().Sha256().Take(4).SequenceEqual(addresshash))
throw new FormatException();
return prikey;
}
複製代碼
解密所使用的scrypt參數須要和加密過程相同,否則沒法得出相同的scrypt key,也就沒法解出privateKey。下面是nep2解密流程:
nep6是NEO爲了給不一樣的錢包應用提供統一的數據格式標準而制定的,全部實現了nep6協議的錢包應用,其錢包數據都是能夠通用的。 新建錢包的時候須要指定新錢包的路徑以及名稱:
源碼位置:neo/Implementations/Wallets/NEP6/NEP6Wallet.cs/NEP6Wallet(string path, string name = null)
this.name = name;
this.version = Version.Parse("1.0");
this.Scrypt = ScryptParameters.Default;
this.accounts = new Dictionary<UInt160, NEP6Account>();
this.extra = JObject.Null;
複製代碼
同時,每一個NEP6錢包均可以保存多個NEP6Account對象,也就是說每一個錢包裏能夠有多個地址帳戶。 NEP6的帳戶類裏並不存儲私鑰,而是存儲的加密後的nep2key,用戶在導入nep6錢包後,若是想獲取到帳戶私鑰信息,就須要用戶手動輸入對應帳號的passphrase才能夠。這裏須要注意的是,因爲每一個錢包只有一份Scrypt參數,因此在nep6錢包裏的帳戶是不能指定不一樣的scrypt參數的。 nep6的錢包保存成文件的時候是以json的格式保存的,帳戶轉json的代碼以下:
源碼位置:neo/Implementations/Wallets/NEP6/NEP6Account.cs
public JObject ToJson()
{
JObject account = new JObject();
account["address"] = Wallet.ToAddress(ScriptHash);//地址
account["label"] = Label; //帳戶標籤
account["isDefault"] = IsDefault;
account["lock"] = Lock;
account["key"] = nep2key;//nep2key
account["contract"] = ((NEP6Contract)Contract)?.ToJson();//帳戶合約
account["extra"] = Extra; //補充信息
return account;
}
複製代碼
nep6錢包轉Json代碼以下:
源碼位置:neo/Implementations/Wallets/NEP6/NEP6Wallet.cs
public void Save()
{
JObject wallet = new JObject();
wallet["name"] = name; //錢包名
wallet["version"] = version.ToString(); //錢包版本
wallet["scrypt"] = Scrypt.ToJson(); //scrypt加密參數
wallet["accounts"] = new JArray(accounts.Values.Select(p => p.ToJson()));//帳戶轉json
wallet["extra"] = extra;
File.WriteAllText(path, wallet.ToString());
}
複製代碼
以上就是NEO建立帳戶及錢包管理帳戶的所有內容,因爲本人技術有限,不免疏漏錯誤之處,萬望多多指教。 另外,本人開發的NEO微信錢包小程序已經上線微信小程序商城,你們能夠搜索 「NEO」進入錢包試用。小程序基於NEL ThinSDK-ts進行開發,源碼發佈於NEL github倉庫, 地址是 :
小程序錢包主要功能基本完成並測試經過,可是尤待優化補充歡迎各位提交代碼或者提出寶貴意見。若是您須要GAS或者NEO進行小程序的測試,能夠發郵件到 jinghui@wayne.edu 聯繫我,我能夠給您轉一些測試網的GAS。
最後,本文發佈以後我會着手NEP協議的漢化,但願感興趣的朋友幫助我一塊兒完成這個任務:github.com/Liaojinghui…