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钱包node

0x01 私钥

和几乎全部的加密货币同样,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)
}

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仓库, 地址是 :

https://github.com/NewEconoLab/NeoWalletForWeChat

小程序钱包主要功能基本完成并测试经过,可是尤待优化补充欢迎各位提交代码或者提出宝贵意见。若是您须要GAS或者NEO进行小程序的测试,能够发邮件到 jinghui@wayne.edu 联系我,我能够给您转一些测试网的GAS。

最后,本文发布以后我会着手NEP协议的汉化,但愿感兴趣的朋友帮助我一块儿完成这个任务:https://github.com/Liaojinghui/proposals

相关文章
相关标签/搜索