做者:freewindnode
比原項目倉庫:react
Github地址:https://github.com/Bytom/bytomgit
Gitee地址:https://gitee.com/BytomBlockc...github
在上一篇咱們已經知道了比原是如何監聽節點的p2p端口,本篇就要繼續在上篇中提到的問題:咱們如何成功的鏈接上比原的節點,而且經過身份驗證,以便後續繼續交換數據?golang
在上一篇中,咱們的比原節點是以solonet
這個chain_id
啓動的,它監聽的是46658
端口。咱們可使用telnet
連上它:算法
$ telnet localhost 46658 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. ט�S��%�z?��_�端��݂���U[e
能夠看到,它發過來了一些亂碼。這些亂碼是什麼意思?咱們應該怎麼應答它?這是本篇將要回答的問題。編程
首先咱們得定位到比原向剛鏈接上來的節點發送數據的地方。說實話,這裏實在是太繞了,山路十八彎,每次我想找到這段代碼,都須要花好一陣功夫。因此下面這段流程,我以爲你之後可能常常會過來看看。數組
總的來講,在比原中有一個Switch
類,它用於集中處理節點與外界交互的邏輯,而它的建立和啓動,又都是在SyncManager
中進行的。另外,監聽p2p端口並拿到相應的鏈接對象的操做,與跟鏈接的對象進行數據交互的操做,又是分開的,前者是在建立SyncManager
的時候進行的,後者是在SyncManager
的啓動(Start
)方法裏交由Switch
進行的。因此整體來講,這一塊邏輯有點複雜(亂),繞來繞去的。安全
這裏不先評價代碼的好壞,咱們仍是先把比原的處理邏輯搞清楚吧。bash
下面仍是從啓動開始,可是因爲咱們在前面已經出現過屢次,因此我會盡可能把不須要的代碼省略掉,帶着你們快速到達目的地,而後再詳細分析。
首先是bytomd node
的入口函數:
func main() { cmd := cli.PrepareBaseCmd(commands.RootCmd, "TM", os.ExpandEnv(config.DefaultDataDir())) cmd.Execute() }
轉交給處理參數node
的函數:
cmd/bytomd/commands/run_node.go#L41
func runNode(cmd *cobra.Command, args []string) error { // Create & start node n := node.NewNode(config) if _, err := n.Start(); err != nil { // ... }
如前一篇所述,「監聽端口」的操做是在node.NewNode(config)
中完成的,此次發送數據的任務是在n.Start()
中進行的。
可是咱們仍是須要看一下node.NewNode
,由於它裏在建立SyncManager
對象的時候,生成了一個供當前鏈接使用的私鑰,它會在後面用到,用於產生公鑰。
func NewNode(config *cfg.Config) *Node { // ... syncManager, _ := netsync.NewSyncManager(config, chain, txPool, newBlockCh) // ... }
func NewSyncManager(config *cfg.Config, chain *core.Chain, txPool *core.TxPool, newBlockCh chan *bc.Hash) (*SyncManager, error) { manager := &SyncManager{ txPool: txPool, chain: chain, privKey: crypto.GenPrivKeyEd25519(), // ... }
就是這個privKey
,它是經過ed25519
生成的,後面會用到。這個私鑰僅在本次鏈接中使用,每一個鏈接都會生成一個新的。
讓咱們再回到主線runNode
,其中n.Start
又將被轉交到Node
的OnStart
方法:
func (n *Node) OnStart() error { // ... n.syncManager.Start() // ... }
轉交到SyncManager
的Start
方法:
func (sm *SyncManager) Start() { go sm.netStart() // ... }
而後在另外一個例程(goroutine)中調用了netStart()
方法:
func (sm *SyncManager) netStart() error { // Start the switch _, err := sm.sw.Start() // ... }
在這裏終於調用了Switch
的Start
方法(sm.sw
中的sw
就是一個Switch
對象):
func (sw *Switch) OnStart() error { // ... // Start listeners for _, listener := range sw.listeners { go sw.listenerRoutine(listener) } // ... }
這裏的sw.listeners
,就包含了監聽p2p端口的listener。而後調用listenerRoutine()
方法,感受快到了。
func (sw *Switch) listenerRoutine(l Listener) { // ... err := sw.addPeerWithConnectionAndConfig(inConn, sw.peerConfig) // ... }
在這裏拿到了鏈接到p2p端口的鏈接對象inConn
們,傳入一堆參數,準備大刑伺候:
func (sw *Switch) addPeerWithConnectionAndConfig(conn net.Conn, config *PeerConfig) error { // ... peer, err := newInboundPeerWithConfig(conn, sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodePrivKey, config) // ... }
把須要的參數細化出來,再次傳入:
func newInboundPeerWithConfig(conn net.Conn, reactorsByCh map[byte]Reactor, chDescs []*ChannelDescriptor, onPeerError func(*Peer, interface{}), ourNodePrivKey crypto.PrivKeyEd25519, config *PeerConfig) (*Peer, error) { return newPeerFromConnAndConfig(conn, false, reactorsByCh, chDescs, onPeerError, ourNodePrivKey, config) }
再繼續,立刻就到了。
func newPeerFromConnAndConfig(rawConn net.Conn, outbound bool, reactorsByCh map[byte]Reactor, chDescs []*ChannelDescriptor, onPeerError func(*Peer, interface{}), ourNodePrivKey crypto.PrivKeyEd25519, config *PeerConfig) (*Peer, error) { // ... // Encrypt connection if config.AuthEnc { // ... conn, err = MakeSecretConnection(conn, ourNodePrivKey) // ... } // ... }
終於到了關鍵的函數MakeSecretConnection()
了。因爲config.AuthEnc
的默認值是true
,因此若是沒有特別設置的話,它就會進入MakeSecretConnection
,在這裏完成身份驗證等各類操做,它也是咱們本篇講解的重點。
好,下面咱們開始。
這個函數的邏輯看起來是至關複雜的,引入了不少密鑰和各類加解密,還屢次跟相應的peer進行數據發送和接收,若是不明白它爲何要這麼作,是很難理解清楚的。好在一旦理解之後,明白了它的意圖,整個就簡單了。
總的來講,比原的節點之間的數據交互,是須要很高的安全性的,尤爲是數據不能明文傳送,不然一旦遇到了壞的「中間人」(能夠理解爲數據從一個節點到另外一個節點中途須要通過的各類網關、路由器、代理等等),數據就有可能被竊取甚至修改。考慮一下這個場景:用戶A想把100萬個比原從本身的賬號轉到用戶B的賬戶,結果信息被中間人修改,最後轉到了中間人指定的賬戶C,那麼這損失就大了,甚至沒法追回。(有同窗問,「區塊鏈上的每一個交易不是會有多個節點驗證嗎?若是隻有單一節點使壞,應該不會生效吧」。我考慮的是這樣一種狀況,好比某用戶在筆記本上運行比原節點,而後在公開場合上網,使用了黑客提供的wifi。那麼該節點與其它結點的全部鏈接均可以被中間人攻擊,廣播出去的交易能夠同時被修改,這樣其它節點拿到的都是修改後的交易。至於這種方法是否能夠生效,還須要我讀完更多的代碼才能肯定,這裏暫時算是一個猜測吧,等我之後再來確認)
因此比原節點之間傳輸信息的時候是加密的,使用了某些非對稱加密的方法。這些方法須要在最開始的時候,節點雙方都把本身的公鑰轉給對方,以後再發信息時就可使用對方的公鑰加密,再由對方使用私鑰解密。加密後的數據,雖然還會通過各類中間人的轉發才能到達對方,可是隻要中間人沒有在最開始拿到雙方的明文公鑰並替換成本身的假冒公鑰,它就沒有辦法知道真實的數據是什麼,也就沒有辦法竊取或修改。
因此這個函數的最終目的,就是:把本身的公鑰安全的發送給對方,同時安全得拿到對方的公鑰。
若是僅僅是發送公鑰,那本質上就是發送一些字節數據過去,應該很簡單。可是比原爲了達到安全的目的,還進行了以下的思考:
challenge
)和加解密時須要的參數(sharedSecret
, sendNonce/recvNonce
)另外還有一些過分的考慮:
sendNonce
和recvNonce
保持同步改變我之因此認爲這些是「過分」的考慮,是由於在這個交互過程當中,數據的長度是固定的,而且很短(只有100多個字節),根本不須要考慮分塊。另外公鑰和簽名數據就是兩個簡單的、長度固定的字節數組,而且只在這裏用一次,我以爲能夠直接發送兩個數組便可,包裝成對象及序列化後,咱們還須要考慮序列化以後的數組長度是如何變化的。
在查閱了相關的代碼之後,我發現這一處邏輯只在這裏使用了一次,沒有必要提早考慮到通用但更復雜的狀況,提早編碼。畢竟那些狀況有可能永遠不會發生,而提早寫好的代碼所增長的複雜度以及可能多出來的bug倒是永遠存在了。
《敏捷軟件開發 原則、模式和實踐》這本書告訴咱們:不要預先設計,儘可能用簡單的辦法實現,等到變化真的到來了,再考慮如何重構讓它適應這種變化。
下面講解「MakeSecretConnection」,因爲該方法有點長,因此會分紅幾塊:
func MakeSecretConnection(conn io.ReadWriteCloser, locPrivKey crypto.PrivKeyEd25519) (*SecretConnection, error) { locPubKey := locPrivKey.PubKey().Unwrap().(crypto.PubKeyEd25519)
首先注意的是參數locPrivKey
,它就是在前面最開始的時候,在SyncManager
中生成的用於本次鏈接通訊的私鑰。而後根據該私鑰,生成對應的公鑰,對於同一個私鑰,生成的公鑰老是相同的。
這個私鑰的長度是64字節,公鑰是32字節,可見二者不是同樣長的。公鑰短一些,更適合加密(速度快一點)。
呆會兒在最後會使用該私鑰對一段數據進行簽名,而後跟這個公鑰一塊兒,通過加密後發送給peer,讓他驗證。成功以後,對方會一直持有這個公鑰,向咱們發送數據前會用它對數據進行加密。
接着,
// Generate ephemeral keys for perfect forward secrecy. locEphPub, locEphPriv := genEphKeys()
這裏生成了一對一次性的公私鑰,用於本次鏈接中對開始那個公鑰(和簽名數據)進行加密。
待會兒會發把這裏生成的locEphPub
以明文的方式傳給對方(爲何是明文?由於必須得有一次明文發送,否則對方一開始就拿到加密的數據無法解開),它就咱們在本文開始經過telnet localhost 46658
時收到的那一堆亂碼。
genEphKeys()
,對應於:
func genEphKeys() (ephPub, ephPriv *[32]byte) { var err error ephPub, ephPriv, err = box.GenerateKey(crand.Reader) if err != nil { cmn.PanicCrisis("Could not generate ephemeral keypairs") } return }
它調用了golang.org/x/crypto/nacl/box
的GenerateKey
函數,在內部使用了curve25519
算法,生成的兩個key的長度都是32字節。
能夠看到,它跟前面的公私鑰的長度不是徹底同樣的,可見二者使用了不一樣的加密算法。前面的是ed25519
,而這裏是curve25519
。
接着回到MakeSecretConnection
,繼續:
// Write local ephemeral pubkey and receive one too. // NOTE: every 32-byte string is accepted as a Curve25519 public key // (see DJB's Curve25519 paper: http://cr.yp.to/ecdh/curve25519-20060209.pdf) remEphPub, err := shareEphPubKey(conn, locEphPub) if err != nil { return nil, err }
這個shareEphPubKey
就是把剛生成的一次性的locEphPub
發給對方,同時也從對方那裏讀取對方生成的一次性公鑰(長度爲32字節):
func shareEphPubKey(conn io.ReadWriteCloser, locEphPub *[32]byte) (remEphPub *[32]byte, err error) { var err1, err2 error cmn.Parallel( func() { _, err1 = conn.Write(locEphPub[:]) }, func() { remEphPub = new([32]byte) _, err2 = io.ReadFull(conn, remEphPub[:]) }, ) if err1 != nil { return nil, err1 } if err2 != nil { return nil, err2 } return remEphPub, nil }
因爲MakeSecretConnection
這個函數,是兩個比原節點在創建起p2p鏈接時都會執行的,因此二者要作的事情都是同樣的。若是我發了數據,則對方也會發相應的數據,而後兩邊都須要讀取。因此我發了什麼樣的數據,我也要同時拿到什麼樣的數據。
再回想本文開始提到的telnet localhost 46658
,當咱們接收到那一段亂碼時,也須要給對方發過去32個字節,雙方纔能進行下一步。
再回到MakeSecretConnection
,接着:
// Compute common shared secret. shrSecret := computeSharedSecret(remEphPub, locEphPriv)
雙方拿到對方的一次性公鑰後,都會和本身生成的一次性私鑰(注意,是私鑰)作一個運算,生成一個叫shrSecret
的密鑰在後面使用。怎麼用呢?就是用它來對要發送的公鑰及簽名數據進行加密,以及對對方發過來的公鑰和簽名數據進行解密。
computeSharedSecret
函數對應的代碼是這樣:
func computeSharedSecret(remPubKey, locPrivKey *[32]byte) (shrSecret *[32]byte) { shrSecret = new([32]byte) box.Precompute(shrSecret, remPubKey, locPrivKey) return }
它是經過對方的公鑰和本身的私鑰算出來的。
這裏有一個神奇的地方,就是雙方算出來的shrSecret
是同樣的!也就是說,假設這裏使用該算法(curve25519
)生成了兩對公私鑰:
privateKey1, publicKey1 privateKey2, publicKey2
而且
publicKey2 + privateKey1 ===> sharedSecret1 publicKey1 + privateKey2 ===> sharedSecret2
那麼sharedSecret1
和sharedSecret2
是同樣的,因此雙方纔能夠拿各自算出來的shrSecret
去解密對方的加密數據。
再接着,會根據雙方的一次性公鑰作一些計算,以供後面使用。
// Sort by lexical order. loEphPub, hiEphPub := sort32(locEphPub, remEphPub)
首先是拿對方和本身的一次性公鑰進行排序,這樣兩邊獲得的loEphPub
和hiEphPub
就是同樣的,後面在計算數值時就能獲得相同的值。
而後是計算nonces,
// Generate nonces to use for secretbox. recvNonce, sendNonce := genNonces(loEphPub, hiEphPub, locEphPub == loEphPub)
nonces
和前面的shrSecret
都是在給公鑰和簽名數據加解密時使用的。其中shrSecret
是固定的,而nonce在不一樣的信息之間是應該不一樣的,用於區別信息。
這裏計算出來的recvNonce
與sendNonce
,一個是用於接收數據後解密,一個是用於發送數據時加密。鏈接雙方的這兩個數據都是相反的,也就是說,一方的recvNonce
與另外一方的sendNonce
相等,這樣當一方使用sendNonce
加密後,另外一方纔可使用相同數值的recvNonce
進行解密。
在後面咱們還能夠看到,當一方發送完數據後,其持有的sendNonce
會增2,另外一方接收並解密後,其recvNonce
也會增2,雙方始終保持一致。(爲何是增2而不是增1,後面有解答)
genNonces
的代碼以下:
func genNonces(loPubKey, hiPubKey *[32]byte, locIsLo bool) (recvNonce, sendNonce *[24]byte) { nonce1 := hash24(append(loPubKey[:], hiPubKey[:]...)) nonce2 := new([24]byte) copy(nonce2[:], nonce1[:]) nonce2[len(nonce2)-1] ^= 0x01 if locIsLo { recvNonce = nonce1 sendNonce = nonce2 } else { recvNonce = nonce2 sendNonce = nonce1 } return }
能夠看到,其中的一個nonce就是把前面排序後的loPubKey
和hiPubKey
組合起來,而另外一個nonce就是把最後一個bit的值由0變成1(或者由1變成0),這樣二者就會是一個奇數一個偶數。然後來在對nonce進行自增操做的時候,每次都是增2,這樣就保證了recvNonce
與sendNonce
不會出現相等的狀況,是一個很巧妙的設計。
後面又經過判斷local is loPubKey
,保證了兩邊獲得的recvNonce
與sendNonce
正好相反,且一邊的recvNonce
與另外一邊的sendNonce
正好相等。
再回到MakeSecretConnection
,繼續:
// Generate common challenge to sign. challenge := genChallenge(loEphPub, hiEphPub)
這裏根據loEphPub
和hiEphPub
計算出來challenge
,在後面將會使用本身的私鑰對它進行簽名,再跟公鑰一塊兒發給對方,讓對方驗證。因爲雙方的loEphPub
和hiEphPub
是相等的,因此算出來的challenge
也是相等的。
func genChallenge(loPubKey, hiPubKey *[32]byte) (challenge *[32]byte) { return hash32(append(loPubKey[:], hiPubKey[:]...)) }
能夠看到genChallenge
就是把兩個一次性公鑰放在一塊兒,並作了一個hash操做,獲得了一個32字節的數組。
其中的hash32
採用了SHA256
的算法,它生成摘要的長度就是32個字節。
func hash32(input []byte) (res *[32]byte) { hasher := sha256.New() hasher.Write(input) // does not error resSlice := hasher.Sum(nil) res = new([32]byte) copy(res[:], resSlice) return }
再回到MakeSecretConnection
,繼續:
// Construct SecretConnection. sc := &SecretConnection{ conn: conn, recvBuffer: nil, recvNonce: recvNonce, sendNonce: sendNonce, shrSecret: shrSecret, }
這裏是生成了一個SecretConnection
的對象,把相關的nonces和shrSecret
傳過去,由於呆會兒對公鑰及簽名數據的加解密操做,都放在了那邊,而這幾個參數都是須要用上的。
前面通過了這麼多的準備工做,終於差很少了。下面將會使用本身的私鑰對challenge
數據進行簽名,而後跟本身的公鑰一塊兒發送給對方:
// Sign the challenge bytes for authentication. locSignature := signChallenge(challenge, locPrivKey) // Share (in secret) each other's pubkey & challenge signature authSigMsg, err := shareAuthSignature(sc, locPubKey, locSignature) if err != nil { return nil, err }
其中的signChallenge
就是簡單的使用本身的私鑰對challenge
數據進行簽名,獲得的是一個32字節的摘要:
func signChallenge(challenge *[32]byte, locPrivKey crypto.PrivKeyEd25519) (signature crypto.SignatureEd25519) { signature = locPrivKey.Sign(challenge[:]).Unwrap().(crypto.SignatureEd25519) return }
而在shareAuthSignature
中,則是把本身的公鑰與簽名後的數據locSignature
一塊兒,通過SecretConnection
的加密後傳給對方,也同時從對方那裏讀取他的公鑰和簽名數據,再解密。因爲這一塊代碼涉及的東西比較多(有分塊,加解密,序列化與反序列化),因此放在後面再講。
再而後,
remPubKey, remSignature := authSigMsg.Key, authSigMsg.Sig if !remPubKey.VerifyBytes(challenge[:], remSignature) { return nil, errors.New("Challenge verification failed") }
從對方傳過來的數據中拿出對方的公鑰和對方簽過名的數據,對它們進行驗證。因爲對方在簽名時,使用的challenge
數據和咱們這邊產生的challenge
同樣,因此能夠直接拿出本地的challenge
使用。
最後,若是驗證經過的話,則把對方的公鑰也加到SecretConnection
對象中,供之後使用。
// We've authorized. sc.remPubKey = remPubKey.Unwrap().(crypto.PubKeyEd25519) return sc, nil }
到這裏,咱們就能夠回答最開始的問題了:咱們應該怎樣鏈接一個比原節點呢?
答案就是:
前面說到,當使用本身的私鑰把challenge
簽名獲得locSignature
後,將經過shareAuthSignature
把它和本身的公鑰一塊兒發給對方。它裏作了不少事,咱們在這一節詳細講解一下。
shareAuthSignature
的代碼以下:
func shareAuthSignature(sc *SecretConnection, pubKey crypto.PubKeyEd25519, signature crypto.SignatureEd25519) (*authSigMessage, error) { var recvMsg authSigMessage var err1, err2 error cmn.Parallel( func() { msgBytes := wire.BinaryBytes(authSigMessage{pubKey.Wrap(), signature.Wrap()}) _, err1 = sc.Write(msgBytes) }, func() { readBuffer := make([]byte, authSigMsgSize) _, err2 = io.ReadFull(sc, readBuffer) if err2 != nil { return } n := int(0) // not used. recvMsg = wire.ReadBinary(authSigMessage{}, bytes.NewBuffer(readBuffer), authSigMsgSize, &n, &err2).(authSigMessage) }) if err1 != nil { return nil, err1 } if err2 != nil { return nil, err2 } return &recvMsg, nil }
能夠看到,它作了這樣幾件事:
authSigMessage
對象:authSigMessage{pubKey.Wrap(), signature.Wrap()}
go-wire
的第三方庫,把它序列化成了一個字節數組SecretConnection.Write()
方法,把這個數組發給對方。須要注意的是,在這個方法內部,將對數據進行分塊,並使用Go語言的secretBox.Seal
對數據進行加密。authSigMsgSize
爲常量,值爲const authSigMsgSize = (32 + 1) + (64 + 1)
)SecretConnection
對象中的方法讀取它,同時進行解密go-wire
把它變成一個authSigMessage
對象authSigMessage
返回給調用者MakeSecretConnection
這裏我以爲沒有必要使用go-wire
對數據進行序列化和反序列化,由於要發送的兩個數組長度是肯定的(一個32,一個64),不管是發送仍是讀取,都很容易肯定長度和拆分規則。而引入了go-wire
之後,就須要知道它的工做細節(好比它產生的字節個數是(32 + 1) + (64 + 1)
),而這個複雜性是沒有必要引入的。
SecretConnection
的Read
和Write
在上一段,對於發送數據時的分塊和加解密相關的操做,都放在了SecretConnection
的方法中。好比sc.Write(msgBytes)
和io.ReadFull(sc, readBuffer)
(其中的sc
都是指SecretConnection
對象),用到的就是SecretConnection
的Write
和Read
。
func (sc *SecretConnection) Write(data []byte) (n int, err error) { for 0 < len(data) { var frame []byte = make([]byte, totalFrameSize) var chunk []byte if dataMaxSize < len(data) { chunk = data[:dataMaxSize] data = data[dataMaxSize:] } else { chunk = data data = nil } chunkLength := len(chunk) binary.BigEndian.PutUint16(frame, uint16(chunkLength)) copy(frame[dataLenSize:], chunk) // encrypt the frame var sealedFrame = make([]byte, sealedFrameSize) secretbox.Seal(sealedFrame[:0], frame, sc.sendNonce, sc.shrSecret) // fmt.Printf("secretbox.Seal(sealed:%X,sendNonce:%X,shrSecret:%X\n", sealedFrame, sc.sendNonce, sc.shrSecret) incr2Nonce(sc.sendNonce) // end encryption _, err := sc.conn.Write(sealedFrame) if err != nil { return n, err } else { n += len(chunk) } } return }
在Write
裏面,除了向鏈接對象寫入數據(sc.conn.Write(sealedFrame)
)外,它主要作了三件事:
dataMaxSize
,即1024
),則要把它分紅多個塊。因爲最後一個塊的數據可能填不滿,因此每一個塊的最開始要用2個字節寫入本塊中實際數據的長度。secretbox.Seal
方法,對塊數據進行加密,用到了sendNonce
和shrSecret
這兩個參數sendNonce
進行自增操做,這樣可保證每次發送時使用的nonce都不同;另外每次增2,這樣可保證它不會跟recvNonce
重複而SecretConnection
的Read
操做,跟前面正好相反:
func (sc *SecretConnection) Read(data []byte) (n int, err error) { if 0 < len(sc.recvBuffer) { n_ := copy(data, sc.recvBuffer) sc.recvBuffer = sc.recvBuffer[n_:] return } sealedFrame := make([]byte, sealedFrameSize) _, err = io.ReadFull(sc.conn, sealedFrame) if err != nil { return } // decrypt the frame var frame = make([]byte, totalFrameSize) // fmt.Printf("secretbox.Open(sealed:%X,recvNonce:%X,shrSecret:%X\n", sealedFrame, sc.recvNonce, sc.shrSecret) _, ok := secretbox.Open(frame[:0], sealedFrame, sc.recvNonce, sc.shrSecret) if !ok { return n, errors.New("Failed to decrypt SecretConnection") } incr2Nonce(sc.recvNonce) // end decryption var chunkLength = binary.BigEndian.Uint16(frame) // read the first two bytes if chunkLength > dataMaxSize { return 0, errors.New("chunkLength is greater than dataMaxSize") } var chunk = frame[dataLenSize : dataLenSize+chunkLength] n = copy(data, chunk) sc.recvBuffer = chunk[n:] return }
它除了正常的讀取字節外,也是作了三件事:
sealedFrameSize
個字節,並按前兩個字節指定的長度來確認有效數據secretbox.Open
以及recvNonce
和shrSecret
這兩個參數recvNonce
進行自增2的操做,以便與對方的sendNonce
保持一致,供下次解密使用須要注意的是,這個函數返回的n
(已讀取數據),是指的解密以後的,因此要比真實讀取的數據小一點。另外,在前面的shareAuthSignature
中,使用的是io.ReadFull(sc)
,而且要讀滿authSigMsgSize
個字節,因此假如數據過長的話,這個Read
方法可能要被調用屢次。
在這一塊,因爲做者假設了發送的數據的長度可能過長,因此才須要這麼複雜的分塊操做,而其實是不須要的。若是咱們簡單點處理,是能夠作到如下兩個簡化的:
recvNonce
和sendNonce
,直接給個常量便可,反正只用一次,不會存在衝突邏輯能夠簡單不少。並且我查了一下,這塊代碼在整個項目中,目前只使用了一次。若是將來真的須要,到時候再加也不遲。
從上面的分析咱們能夠看到,比原爲了保證節點間通訊的安全性,是作了大量的工做的。那麼,當前的作法,是否能夠徹底杜絕中間人攻擊呢?
按個人理解,仍是不行的,由於若是有人徹底清楚了比原的驗證流程,仍是能夠寫出相應的工具。好比,中間人能夠按照下面的方式:
這個過程可使用下圖來輔助理解:
那麼這是否說明比原的作法白作了呢?不,我認爲比原的作法已經夠用了。
按我目前的瞭解,對於防範中間人,並無徹底完美的辦法(由於如何保證安全的把公鑰經過網絡發送給另外一方自己就是一個充滿挑戰的問題),目前多數是證書等作法。對於比原來講,若是採用這種作法,會讓節點的部署和維護麻煩不少。而目前的作法,雖然不能徹底杜絕,可是其實已經解決了大部分的問題:
我以爲這基本上就杜絕了一大撥技術能力不過關的騙子。只要咱們在使用的時候,再注意防範(好比不使用不安全的網絡或者代理),我以爲基本上就沒什麼問題了。
最後,把我閱讀這段代碼過程當中畫的流程圖分享出來,也許對你本身閱讀的時候有幫助: