NEO从源码分析看nep2与nep6
0x00 前言
混社區的時候(QQ群)總是聽到大佬們聊到nep,好奇心驅使下就去neo官網找資料,然鵝,什么都沒找到。后來就請教大佬,才知道nep是neo一系列提案,文檔并不在neo官網,在這里。但是很奇怪的是我到目前為止只聽說到了nep2,nep5和nep6,其余的幾個提案似乎沒什么人講,以后有機會我再仔細了解下。nep2提案是一套加密私鑰的算法,nep5提案是發布token相關的,nep6則是定義了標準化的neo錢包數據結構。由于我現在了解的最詳盡的是nep2和nep6(好幾個sdk源碼都擼了一遍),而且nep2和nep6也是相輔相成密不可分,所以這里我就先主要從源碼角度分析下nep2和nep6. 注: 本文行文邏輯 新賬戶 => nep2加解密 => 添加到nep6錢包
0x01 私鑰
和幾乎所有的加密貨幣一樣,NEO的賬戶也是用了基于橢圓曲線的公私鑰生成算法,在NEO的賬戶體系中,公鑰由私鑰計算而來,地址又由公鑰計算而來,可以說只要掌握了私鑰,就完全掌握了這個賬戶。數學原理請移步這里下載密碼學書籍學習。 NEO的私鑰是隨機生成的長度為32的字節數組:
源碼位置:neo/Wallets/Wallet.cs/CreateAccount()
byte[] privateKey = new byte[32];using (RandomNumberGenerator rng = RandomNumberGenerator.Create()){rng.GetBytes(privateKey);} 復制代碼由于各個節點新賬戶的生成完全在本地進行,所以必須保證隨機數生成器完全隨機也就是安全隨機才能真正確保賬戶的唯一性以及安全性,這里我研究了不同平臺采取的安全隨機數策略,首先就是neo內核C#版本采用的RandomNumberGenerator類,這個隨機數生成算法以當前系統runtime環境參數作為熵源產生隨機數,雖然執行效率比System.Random要慢上兩個數量級,但是產生的結果卻是安全的。
這里我還想說一下我在開發NEO錢包小程序的時候遇到的問題,那就是微信小程序并不提供安全的隨機數生成算法,同時也不支持node內置的crypto,這讓我糾結了很久,因為沒有安全的隨機數生成算法,那么這個錢包幾乎就是不可用的。我曾想過:
- 用戶當前的經緯度,加速度,海拔
- 用戶拍照并對照片進行哈希
等方法來作為熵源,但是第一種密鑰空間太小,第二種沒辦法實現。后來我發現在每次獲取用戶授權數據的時候,會收到一段加密的字符串。我研究了下這個加密算法,主要是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) } 復制代碼0x02 公鑰
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的,所以我就不貼了。
0x03 地址
前文已經說過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之后的摘要生成之外,兩者的地址計算過程幾乎一摸一樣。
0x04 nep2
上文中已經從私鑰到地址的整個流程都分析完了,如果是使用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 keybyte[] 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解密流程:
0x05 nep6
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;//nep2keyaccount["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()));//賬戶轉jsonwallet["extra"] = extra;File.WriteAllText(path, wallet.ToString());} 復制代碼以上就是NEO創建賬戶及錢包管理賬戶的全部內容,由于本人技術有限,難免疏漏錯誤之處,萬望多多指教。 另外,本人開發的NEO微信錢包小程序已經上線微信小程序商城,大家可以搜索 “NEO”進入錢包試用。小程序基于NEL ThinSDK-ts進行開發,源碼發布于NEL github倉庫, 地址是 :
github.com/NewEconoLab…
小程序錢包主要功能基本完成并測試通過,但是尤待優化補充歡迎各位提交代碼或者提出寶貴意見。如果您需要GAS或者NEO進行小程序的測試,可以發郵件到 jinghui@wayne.edu 聯系我,我可以給您轉一些測試網的GAS。
最后,本文發布之后我會著手NEP協議的漢化,希望感興趣的朋友幫助我一起完成這個任務:github.com/Liaojinghui…
進技術群交流:795681763原文鏈接:my.oschina.net/u/2276921/b…
總結
以上是生活随笔為你收集整理的NEO从源码分析看nep2与nep6的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 香港科技园公司“牵手”腾讯 共推香港金融
- 下一篇: 算法笔记-判断链表保存的字符串是否是回文