// Sign known message: static-shared-secret ^ nonce // This place should be directly using static sharing secrets. A Shared Secret generated by using your private key and the public key of the other party. Token, err = H. staticsharedsecret (PRV) If Err! = Nil {return nil, err} // here I understand that this initnonce is encrypted by sharing secrets. Signed: = XOR (token, H. initnonce) // use a random private key to encrypt this information. Signature, err: = crypto. Sign (signed, H. randomprivkey. exportecdsa () If Err! = Nil {return nil, err} MSG: = new (authmsgv4) Copy (msg. Signature [:], signature) // inform the initiator of the public key here. In this way, the other party can use its own private key and the public key to generate static shared secrets. Copy (MSG. initiatorpubkey [:], crypto. fromecdsapub (& Prv. publickey) [1:]) Copy (MSG. nonce [:], H. initnonce) MSG. version = 4 return MSG, nil} // staticsharedsecret returns the static Shared Secret, The result // of key agreement between the local and remote static node key. func (H * enchandshake) staticsharedsecret (PRV * ECDSA. privatekey) ([] Byte, error) {return ecies. importecdsa (PRV ). generateshared (H. remotepub, ssklen, ssklen )}
Sealeip8 method. This method is a group package method that performs RLP encoding on MSG. Fill in some data. Then, use the public key of the other party to encrypt the data. This means that only the private key of the other party can decrypt this information.
func sealEIP8(msg interface{}, h *encHandshake) ([]byte, error) { buf := new(bytes.Buffer) if err := rlp.Encode(buf, msg); err != nil { return nil, err } // pad with random amount of data. the amount needs to be at least 100 bytes to make // the message distinguishable from pre-EIP-8 handshakes. pad := padSpace[:mrand.Intn(len(padSpace)-100)+100] buf.Write(pad) prefix := make([]byte, 2) binary.BigEndian.PutUint16(prefix, uint16(buf.Len()+eciesOverhead)) enc, err := ecies.Encrypt(rand.Reader, h.remotePub, buf.Bytes(), nil, prefix) return append(prefix, enc...), err}
Readhandshakemsg is called in two places. One is in initiatorenchandshake. One is in receiverenchandshake. This method is relatively simple. First, try Decoding in a format. If not, use another method. It should be a compatibility setting. Basically, it is to use its own private key for decoding and then call RLP to decode it into a struct. The structure description is the authrespv4 below. The most important thing in it is the random public key of the Peer end. Both parties can share the same secret through their own private key and the random public key at the peer end. However, this shared secret cannot be obtained by a third party.
// RLPx v4 handshake response (defined in EIP-8).type authRespV4 struct { RandomPubkey [pubLen]byte Nonce [shaLen]byte Version uint // Ignore additional fields (forward-compatibility) Rest []rlp.RawValue `rlp:"tail"`}func readHandshakeMsg(msg plainDecoder, plainSize int, prv *ecdsa.PrivateKey, r io.Reader) ([]byte, error) { buf := make([]byte, plainSize) if _, err := io.ReadFull(r, buf); err != nil { return buf, err } // Attempt decoding pre-EIP-8 "plain" format. key := ecies.ImportECDSA(prv) if dec, err := key.Decrypt(rand.Reader, buf, nil, nil); err == nil { msg.decodePlain(dec) return buf, nil } // Could be EIP-8 format, try that. prefix := buf[:2] size := binary.BigEndian.Uint16(prefix) if size < uint16(plainSize) { return buf, fmt.Errorf("size underflow, need at least %d bytes", plainSize) } buf = append(buf, make([]byte, size-uint16(plainSize)+2)...) if _, err := io.ReadFull(r, buf[plainSize:]); err != nil { return buf, err } dec, err := key.Decrypt(rand.Reader, buf[2:], nil, prefix) if err != nil { return buf, err } // Can‘t use rlp.DecodeBytes here because it rejects // trailing data (forward-compatibility). s := rlp.NewStream(bytes.NewReader(dec), 0) return buf, s.Decode(msg)}
Handleauthresp this method is very simple.
Func (HEnchandshake) handleauthresp (msgAuthrespv4) (ERR error ){
H. respnonce = msg. nonce [:]
H. remoterandompub, err = importpublickey (msg. randompubkey [:])
Return err
}
The last is the secrets function, which is called after handshake is complete. It generates a sharing secret through its random private key and the public key at the peer end. This sharing secret is instantaneous (only exists in the current link ). So one day, the private key is cracked. The previous messages are still secure.
// Secrets is called after the handshake is completed.
// It extracts the connection secrets from the handshake values.
Func (H * enchandshake) secrets (auth, authresp [] Byte) (secrets, error ){
Ecdhesecret, err: = H. randomprivkey. generateshared (H. remoterandompub, ssklen, ssklen)
If Err! = Nil {
Return secrets {}, err
}
// Derive base secrets from ephemeral Key Agreement sharedsecret: = crypto. keccak256 (ecdhesecret, crypto. keccak256 (H. respnonce, H. initnonce) aessecret: = crypto. keccak256 (ecdhesecret, sharedsecret) // The Mac actually protects the sharing secret of ecdhesecret. Respnonce and initnonce values: S: = secrets {remoteid: H. remoteid, AES: aessecret, Mac: crypto. keccak256 (ecdhesecret, aessecret),} // setup sha3 instances for the MACs mac1: = sha3.newkeccak256 () mac1.write (XOR (S. mac, H. respnonce) mac1.write (auth) mac2: = sha3.newkeccak256 () mac2.write (XOR (S. mac, H. initnonce) mac2.write (authresp) // each packet received will check whether its MAC value meets the calculation result. If not, the problem occurs. If H. initiator {S. egressmac, S. ingressmac = mac1, mac2} else {S. egressmac, S. ingressmac = mac2, mac1} return S, nil}
The content of the receiverenchandshake function is roughly the same as that of initiatorenchandshake. But the order is somewhat different.
// receiverEncHandshake negotiates a session token on conn.// it should be called on the listening side of the connection.//// prv is the local client‘s private key.// token is the token from a previous session with this node.func receiverEncHandshake(conn io.ReadWriter, prv *ecdsa.PrivateKey, token []byte) (s secrets, err error) { authMsg := new(authMsgV4) authPacket, err := readHandshakeMsg(authMsg, encAuthMsgLen, prv, conn) if err != nil { return s, err } h := new(encHandshake) if err := h.handleAuthMsg(authMsg, prv); err != nil { return s, err } authRespMsg, err := h.makeAuthResp() if err != nil { return s, err } var authRespPacket []byte if authMsg.gotPlain { authRespPacket, err = authRespMsg.sealPlain(h) } else { authRespPacket, err = sealEIP8(authRespMsg, h) } if err != nil { return s, err } if _, err = conn.Write(authRespPacket); err != nil { return s, err } return h.secrets(authPacket, authRespPacket)}
Doprotocolhandshake
This method is simple and the encrypted channel has been created. We can see that we only agree whether to use snappy encryption and then exit.
// Doenchandshake runs the protocol handshake using authenticated
// Messages. The protocol handshake is the first authenticated message
// And also verifies whether the encryption handshake 'worked' and
// Remote side actually provided the right public key.
Func (TRlpx) doprotohandshake (ourProtohandshake) (their * protohandshake, err error ){
// Writing our handshake happens concurrently, we prefer
// Returning the handshake read error. If the remote side
// Disconnects us early with a valid reason, we shoshould return it
// As the error so it can be tracked elsewhere.
Werr: = make (Chan error, 1)
Go func () {werr <-send (T. RW, handshakemsg, our )}()
If their, err = readprotocolhandshake (T. RW, our); Err! = Nil {
<-Werr // make sure the write terminates too
Return nil, err
}
If err: = <-werr; Err! = Nil {
Return nil, FMT. errorf ("write error: % v", err)
}
// If the Protocol version supports snappy encoding, upgrade immediately
T. RW. Snappy = their. version> = snappyprotocolversion
return their, nil}
Rlpxframerw data frame Separation
Data frame splitting is mainly completed through the rlpxframerw class.
// Rlpxframerw implements a simplified version of rlpx framing.
// Chunked messages are not supported and all headers are equal
// Zeroheader.
//
// Rlpxframerw is not safe for concurrent use from multiple goroutines.
Type rlpxframerw struct {
Conn Io. readwriter
ENC cipher. Stream
Dec cipher. Stream
macCipher cipher.Block egressMAC hash.Hash ingressMAC hash.Hash snappy bool}
After the two handshakes are completed. Call the newrlpxframerw method to create this object.
t.rw = newRLPXFrameRW(t.fd, sec)
Then the readmsg and writemsg methods are provided. The two methods directly call the readmsg and writemsg of rlpxframerw.
func (t *rlpx) ReadMsg() (Msg, error) { t.rmu.Lock() defer t.rmu.Unlock() t.fd.SetReadDeadline(time.Now().Add(frameReadTimeout)) return t.rw.ReadMsg()}func (t *rlpx) WriteMsg(msg Msg) error { t.wmu.Lock() defer t.wmu.Unlock() t.fd.SetWriteDeadline(time.Now().Add(frameWriteTimeout)) return t.rw.WriteMsg(msg)}
Writemsg
func (rw *rlpxFrameRW) WriteMsg(msg Msg) error { ptype, _ := rlp.EncodeToBytes(msg.Code) // if snappy is enabled, compress message now if rw.snappy { if msg.Size > maxUint24 { return errPlainMessageTooLarge } payload, _ := ioutil.ReadAll(msg.Payload) payload = snappy.Encode(nil, payload) msg.Payload = bytes.NewReader(payload) msg.Size = uint32(len(payload)) } // write header headbuf := make([]byte, 32) fsize := uint32(len(ptype)) + msg.Size if fsize > maxUint24 { return errors.New("message size overflows uint24") } putInt24(fsize, headbuf) // TODO: check overflow copy(headbuf[3:], zeroHeader) rw.enc.XORKeyStream(headbuf[:16], headbuf[:16]) // first half is now encrypted // write header MAC copy(headbuf[16:], updateMAC(rw.egressMAC, rw.macCipher, headbuf[:16])) if _, err := rw.conn.Write(headbuf); err != nil { return err } // write encrypted frame, updating the egress MAC hash with // the data written to conn. tee := cipher.StreamWriter{S: rw.enc, W: io.MultiWriter(rw.conn, rw.egressMAC)} if _, err := tee.Write(ptype); err != nil { return err } if _, err := io.Copy(tee, msg.Payload); err != nil { return err } if padding := fsize % 16; padding > 0 { if _, err := tee.Write(zero16[:16-padding]); err != nil { return err } } // write frame MAC. egress MAC hash is up to date because // frame content was written to it as well. fmacseed := rw.egressMAC.Sum(nil) mac := updateMAC(rw.egressMAC, rw.macCipher, fmacseed) _, err := rw.conn.Write(mac) return err}
Readmsg
func (rw *rlpxFrameRW) ReadMsg() (msg Msg, err error) { // read the header headbuf := make([]byte, 32) if _, err := io.ReadFull(rw.conn, headbuf); err != nil { return msg, err } // verify header mac shouldMAC := updateMAC(rw.ingressMAC, rw.macCipher, headbuf[:16]) if !hmac.Equal(shouldMAC, headbuf[16:]) { return msg, errors.New("bad header MAC") } rw.dec.XORKeyStream(headbuf[:16], headbuf[:16]) // first half is now decrypted fsize := readInt24(headbuf) // ignore protocol type for now // read the frame content var rsize = fsize // frame size rounded up to 16 byte boundary if padding := fsize % 16; padding > 0 { rsize += 16 - padding } framebuf := make([]byte, rsize) if _, err := io.ReadFull(rw.conn, framebuf); err != nil { return msg, err } // read and validate frame MAC. we can re-use headbuf for that. rw.ingressMAC.Write(framebuf) fmacseed := rw.ingressMAC.Sum(nil) if _, err := io.ReadFull(rw.conn, headbuf[:16]); err != nil { return msg, err } shouldMAC = updateMAC(rw.ingressMAC, rw.macCipher, fmacseed) if !hmac.Equal(shouldMAC, headbuf[:16]) { return msg, errors.New("bad frame MAC") } // decrypt frame content rw.dec.XORKeyStream(framebuf, framebuf) // decode message code content := bytes.NewReader(framebuf[:fsize]) if err := rlp.Decode(content, &msg.Code); err != nil { return msg, err } msg.Size = uint32(content.Len()) msg.Payload = content // if snappy is enabled, verify and decompress message if rw.snappy { payload, err := ioutil.ReadAll(msg.Payload) if err != nil { return msg, err } size, err := snappy.DecodedLen(payload) if err != nil { return msg, err } if size > int(maxUint24) { return msg, errPlainMessageTooLarge } payload, err = snappy.Decode(nil, payload) if err != nil { return msg, err } msg.Size, msg.Payload = uint32(size), bytes.NewReader(payload) } return msg, nil}
Frame Structure
normal = not chunked chunked-0 = First frame of a multi-frame packet chunked-n = Subsequent frames for multi-frame packet || is concatenate ^ is xorSingle-frame packet:header || header-mac || frame || frame-macMulti-frame packet:header || header-mac || frame-0 ||[ header || header-mac || frame-n || ... || ]header || header-mac || frame-last || frame-macheader: frame-size || header-data || paddingframe-size: 3-byte integer size of frame, big endian encoded (excludes padding)header-data: normal: rlp.list(protocol-type[, context-id]) chunked-0: rlp.list(protocol-type, context-id, total-packet-size) chunked-n: rlp.list(protocol-type, context-id) values: protocol-type: < 2**16 context-id: < 2**16 (optional for normal frames) total-packet-size: < 2**32padding: zero-fill to 16-byte boundaryheader-mac: right128 of egress-mac.update(aes(mac-secret,egress-mac) ^ header-ciphertext).digestframe: normal: rlp(packet-type) [|| rlp(packet-data)] || padding chunked-0: rlp(packet-type) || rlp(packet-data...) chunked-n: rlp(...packet-data) || paddingpadding: zero-fill to 16-byte boundary (only necessary for last frame)frame-mac: right128 of egress-mac.update(aes(mac-secret,egress-mac) ^ right128(egress-mac.update(frame-ciphertext).digest))egress-mac: h256, continuously updated with egress-bytes*ingress-mac: h256, continuously updated with ingress-bytes*
I am not familiar with encryption and decryption algorithms, so the analysis here is not thorough. The general process is analyzed for the moment. There are still many details not confirmed.
Blockchain getting started ethereum source code analysis encryption link between p2p-rlpx nodes 2