NEO從原始碼分析看nep2與nep6

NEOGO發表於2019-01-30

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地址流程 BitCoin地址流程

第一張比較醜的流程圖是我畫的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 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。加密流程圖如下:

nep2加密流程

而解密的過程則是和加密相反:

原始碼位置: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解密流程:

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;//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倉庫, 地址是 :

github.com/NewEconoLab…

小程式錢包主要功能基本完成並測試通過,但是尤待優化補充歡迎各位提交程式碼或者提出寶貴意見。如果您需要GAS或者NEO進行小程式的測試,可以發郵件到 jinghui@wayne.edu 聯絡我,我可以給您轉一些測試網的GAS。

最後,本文釋出之後我會著手NEP協議的漢化,希望感興趣的朋友幫助我一起完成這個任務:github.com/Liaojinghui…

進技術群交流:795681763


相關文章