mirror of
https://github.com/ipfs/kubo.git
synced 2025-06-30 18:13:54 +08:00
better handshake for all.
This commit is contained in:

committed by
Brian Tiger Chow

parent
67e76c0acc
commit
c787adaa65
8
crypto/spipe/Makefile
Normal file
8
crypto/spipe/Makefile
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
all: message.pb.go
|
||||||
|
|
||||||
|
message.pb.go: message.proto
|
||||||
|
protoc --gogo_out=. --proto_path=../../../../../:/usr/local/opt/protobuf/include:. $<
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm message.pb.go
|
310
crypto/spipe/handshake.go
Normal file
310
crypto/spipe/handshake.go
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
// Package spipe handles establishing secure communication between two peers.
|
||||||
|
|
||||||
|
package spipe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/sha512"
|
||||||
|
"hash"
|
||||||
|
|
||||||
|
proto "github.com/jbenet/go-ipfs/Godeps/_workspace/src/code.google.com/p/goprotobuf/proto"
|
||||||
|
ci "github.com/jbenet/go-ipfs/crypto"
|
||||||
|
peer "github.com/jbenet/go-ipfs/peer"
|
||||||
|
u "github.com/jbenet/go-ipfs/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// List of supported ECDH curves
|
||||||
|
var SupportedExchanges = "P-256,P-224,P-384,P-521"
|
||||||
|
|
||||||
|
// List of supported Ciphers
|
||||||
|
var SupportedCiphers = "AES-256,AES-128"
|
||||||
|
|
||||||
|
// List of supported Hashes
|
||||||
|
var SupportedHashes = "SHA256,SHA512,SHA1"
|
||||||
|
|
||||||
|
// ErrUnsupportedKeyType is returned when a private key cast/type switch fails.
|
||||||
|
var ErrUnsupportedKeyType = errors.New("unsupported key type")
|
||||||
|
|
||||||
|
// ErrClosed signals the closing of a connection.
|
||||||
|
var ErrClosed = errors.New("connection closed")
|
||||||
|
|
||||||
|
// handsahke performs initial communication over insecure channel to share
|
||||||
|
// keys, IDs, and initiate communication.
|
||||||
|
func (s *SecurePipe) handshake() error {
|
||||||
|
// Generate and send Hello packet.
|
||||||
|
// Hello = (rand, PublicKey, Supported)
|
||||||
|
nonce := make([]byte, 16)
|
||||||
|
_, err := rand.Read(nonce)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
myPubKey, err := s.local.PubKey.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
proposeMsg := new(Propose)
|
||||||
|
proposeMsg.Rand = nonce
|
||||||
|
proposeMsg.Pubkey = myPubKey
|
||||||
|
proposeMsg.Exchanges = &SupportedExchanges
|
||||||
|
proposeMsg.Ciphers = &SupportedCiphers
|
||||||
|
proposeMsg.Hashes = &SupportedHashes
|
||||||
|
|
||||||
|
encoded, err := proto.Marshal(proposeMsg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.insecure.Out <- encoded
|
||||||
|
|
||||||
|
// Parse their Propose packet and generate an Exchange packet.
|
||||||
|
// Exchange = (EphemeralPubKey, Signature)
|
||||||
|
var resp []byte
|
||||||
|
select {
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
return ErrClosed
|
||||||
|
case resp = <-s.Duplex.In:
|
||||||
|
}
|
||||||
|
|
||||||
|
proposeResp := new(Propose)
|
||||||
|
err = proto.Unmarshal(resp, proposeResp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.remote.PubKey, err = ci.UnmarshalPublicKey(proposeResp.GetPubkey())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.remote.ID, err = IDFromPubKey(s.remote.PubKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
exchange, err := selectBest(SupportedExchanges, proposeResp.GetExchanges())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cipherType, err := selectBest(SupportedCiphers, proposeResp.GetCiphers())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hashType, err := selectBest(SupportedHashes, proposeResp.GetHashes())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
epubkey, done, err := ci.GenerateEKeyPair(exchange) // Generate EphemeralPubKey
|
||||||
|
|
||||||
|
var handshake bytes.Buffer // Gather corpus to sign.
|
||||||
|
handshake.Write(encoded)
|
||||||
|
handshake.Write(resp)
|
||||||
|
handshake.Write(epubkey)
|
||||||
|
|
||||||
|
exPacket := new(Exchange)
|
||||||
|
|
||||||
|
exPacket.Epubkey = epubkey
|
||||||
|
exPacket.Signature, err = s.local.PrivKey.Sign(handshake.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
exEncoded, err := proto.Marshal(exPacket)
|
||||||
|
|
||||||
|
s.insecure.Out <- exEncoded
|
||||||
|
|
||||||
|
// Parse their Exchange packet and generate a Finish packet.
|
||||||
|
// Finish = E('Finish')
|
||||||
|
var resp1 []byte
|
||||||
|
select {
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
return ErrClosed
|
||||||
|
case resp1 = <-s.insecure.In:
|
||||||
|
}
|
||||||
|
|
||||||
|
exchangeResp := new(Exchange)
|
||||||
|
err = proto.Unmarshal(resp1, exchangeResp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var theirHandshake bytes.Buffer
|
||||||
|
theirHandshake.Write(resp)
|
||||||
|
theirHandshake.Write(encoded)
|
||||||
|
theirHandshake.Write(exchangeResp.GetEpubkey())
|
||||||
|
|
||||||
|
ok, err := s.remote.PubKey.Verify(theirHandshake.Bytes(), exchangeResp.GetSignature())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return errors.New("Bad signature!")
|
||||||
|
}
|
||||||
|
|
||||||
|
secret, err := done(exchangeResp.GetEpubkey())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmp := bytes.Compare(myPubKey, proposeResp.GetPubkey())
|
||||||
|
mIV, tIV, mCKey, tCKey, mMKey, tMKey := ci.KeyStretcher(cmp, cipherType, hashType, secret)
|
||||||
|
|
||||||
|
go s.handleSecureIn(hashType, tIV, tCKey, tMKey)
|
||||||
|
go s.handleSecureOut(hashType, mIV, mCKey, mMKey)
|
||||||
|
|
||||||
|
finished := []byte("Finished")
|
||||||
|
|
||||||
|
s.Out <- finished
|
||||||
|
var resp2 []byte
|
||||||
|
select {
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
return ErrClosed
|
||||||
|
case resp2 = <-s.Duplex.In:
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.Compare(resp2, finished) != 0 {
|
||||||
|
return errors.New("Negotiation failed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
u.DOut("[%s] identify: Got node id: %s\n", s.local.ID.Pretty(), s.remote.ID.Pretty())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeMac(hashType string, key []byte) (hash.Hash, int) {
|
||||||
|
switch hashType {
|
||||||
|
case "SHA1":
|
||||||
|
return hmac.New(sha1.New, key), sha1.Size
|
||||||
|
case "SHA512":
|
||||||
|
return hmac.New(sha512.New, key), sha512.Size
|
||||||
|
default:
|
||||||
|
return hmac.New(sha256.New, key), sha256.Size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SecurePipe) handleSecureIn(hashType string, tIV, tCKey, tMKey []byte) {
|
||||||
|
theirBlock, _ := aes.NewCipher(tCKey)
|
||||||
|
theirCipher := cipher.NewCTR(theirBlock, tIV)
|
||||||
|
|
||||||
|
theirMac, macSize := makeMac(hashType, tMKey)
|
||||||
|
|
||||||
|
for {
|
||||||
|
data, ok := <-s.insecure.In
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) <= macSize {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mark := len(data) - macSize
|
||||||
|
buff := make([]byte, mark)
|
||||||
|
|
||||||
|
theirCipher.XORKeyStream(buff, data[0:mark])
|
||||||
|
|
||||||
|
theirMac.Write(data[0:mark])
|
||||||
|
expected := theirMac.Sum(nil)
|
||||||
|
theirMac.Reset()
|
||||||
|
|
||||||
|
hmacOk := hmac.Equal(data[mark:], expected)
|
||||||
|
|
||||||
|
if hmacOk {
|
||||||
|
s.Duplex.In <- buff
|
||||||
|
} else {
|
||||||
|
s.Duplex.In <- nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SecurePipe) handleSecureOut(hashType string, mIV, mCKey, mMKey []byte) {
|
||||||
|
myBlock, _ := aes.NewCipher(mCKey)
|
||||||
|
myCipher := cipher.NewCTR(myBlock, mIV)
|
||||||
|
|
||||||
|
myMac, macSize := makeMac(hashType, mMKey)
|
||||||
|
|
||||||
|
for {
|
||||||
|
data, ok := <-s.Out
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
buff := make([]byte, len(data)+macSize)
|
||||||
|
|
||||||
|
myCipher.XORKeyStream(buff, data)
|
||||||
|
|
||||||
|
myMac.Write(buff[0:len(data)])
|
||||||
|
copy(buff[len(data):], myMac.Sum(nil))
|
||||||
|
myMac.Reset()
|
||||||
|
|
||||||
|
s.insecure.Out <- buff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDFromPubKey retrieves a Public Key from the peer given by pk
|
||||||
|
func IDFromPubKey(pk ci.PubKey) (peer.ID, error) {
|
||||||
|
b, err := pk.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
hash, err := u.Hash(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return peer.ID(hash), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determines which algorithm to use. Note: f(a, b) = f(b, a)
|
||||||
|
func selectBest(myPrefs, theirPrefs string) (string, error) {
|
||||||
|
// Person with greatest hash gets first choice.
|
||||||
|
myHash, err := u.Hash([]byte(myPrefs))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
theirHash, err := u.Hash([]byte(theirPrefs))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmp := bytes.Compare(myHash, theirHash)
|
||||||
|
var firstChoiceArr, secChoiceArr []string
|
||||||
|
|
||||||
|
if cmp == -1 {
|
||||||
|
firstChoiceArr = strings.Split(theirPrefs, ",")
|
||||||
|
secChoiceArr = strings.Split(myPrefs, ",")
|
||||||
|
} else if cmp == 1 {
|
||||||
|
firstChoiceArr = strings.Split(myPrefs, ",")
|
||||||
|
secChoiceArr = strings.Split(theirPrefs, ",")
|
||||||
|
} else { // Exact same preferences.
|
||||||
|
myPrefsArr := strings.Split(myPrefs, ",")
|
||||||
|
return myPrefsArr[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, secChoice := range secChoiceArr {
|
||||||
|
for _, firstChoice := range firstChoiceArr {
|
||||||
|
if firstChoice == secChoice {
|
||||||
|
return firstChoice, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errors.New("No algorithms in common!")
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package identify
|
package spipe
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
@ -1,68 +1,70 @@
|
|||||||
// Code generated by protoc-gen-go.
|
// Code generated by protoc-gen-gogo.
|
||||||
// source: message.proto
|
// source: message.proto
|
||||||
// DO NOT EDIT!
|
// DO NOT EDIT!
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Package identify is a generated protocol buffer package.
|
Package spipe is a generated protocol buffer package.
|
||||||
|
|
||||||
It is generated from these files:
|
It is generated from these files:
|
||||||
message.proto
|
message.proto
|
||||||
|
|
||||||
It has these top-level messages:
|
It has these top-level messages:
|
||||||
Hello
|
Propose
|
||||||
Exchange
|
Exchange
|
||||||
*/
|
*/
|
||||||
package identify
|
package spipe
|
||||||
|
|
||||||
import proto "github.com/jbenet/go-ipfs/Godeps/_workspace/src/code.google.com/p/goprotobuf/proto"
|
import proto "code.google.com/p/gogoprotobuf/proto"
|
||||||
|
import json "encoding/json"
|
||||||
import math "math"
|
import math "math"
|
||||||
|
|
||||||
// Reference imports to suppress errors if they are not otherwise used.
|
// Reference proto, json, and math imports to suppress error if they are not otherwise used.
|
||||||
var _ = proto.Marshal
|
var _ = proto.Marshal
|
||||||
|
var _ = &json.SyntaxError{}
|
||||||
var _ = math.Inf
|
var _ = math.Inf
|
||||||
|
|
||||||
type Hello struct {
|
type Propose struct {
|
||||||
Rand []byte `protobuf:"bytes,1,req,name=rand" json:"rand,omitempty"`
|
Rand []byte `protobuf:"bytes,1,opt,name=rand" json:"rand,omitempty"`
|
||||||
Pubkey []byte `protobuf:"bytes,2,req,name=pubkey" json:"pubkey,omitempty"`
|
Pubkey []byte `protobuf:"bytes,2,opt,name=pubkey" json:"pubkey,omitempty"`
|
||||||
Exchanges *string `protobuf:"bytes,3,req,name=exchanges" json:"exchanges,omitempty"`
|
Exchanges *string `protobuf:"bytes,3,opt,name=exchanges" json:"exchanges,omitempty"`
|
||||||
Ciphers *string `protobuf:"bytes,4,req,name=ciphers" json:"ciphers,omitempty"`
|
Ciphers *string `protobuf:"bytes,4,opt,name=ciphers" json:"ciphers,omitempty"`
|
||||||
Hashes *string `protobuf:"bytes,5,req,name=hashes" json:"hashes,omitempty"`
|
Hashes *string `protobuf:"bytes,5,opt,name=hashes" json:"hashes,omitempty"`
|
||||||
XXX_unrecognized []byte `json:"-"`
|
XXX_unrecognized []byte `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Hello) Reset() { *m = Hello{} }
|
func (m *Propose) Reset() { *m = Propose{} }
|
||||||
func (m *Hello) String() string { return proto.CompactTextString(m) }
|
func (m *Propose) String() string { return proto.CompactTextString(m) }
|
||||||
func (*Hello) ProtoMessage() {}
|
func (*Propose) ProtoMessage() {}
|
||||||
|
|
||||||
func (m *Hello) GetRand() []byte {
|
func (m *Propose) GetRand() []byte {
|
||||||
if m != nil {
|
if m != nil {
|
||||||
return m.Rand
|
return m.Rand
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Hello) GetPubkey() []byte {
|
func (m *Propose) GetPubkey() []byte {
|
||||||
if m != nil {
|
if m != nil {
|
||||||
return m.Pubkey
|
return m.Pubkey
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Hello) GetExchanges() string {
|
func (m *Propose) GetExchanges() string {
|
||||||
if m != nil && m.Exchanges != nil {
|
if m != nil && m.Exchanges != nil {
|
||||||
return *m.Exchanges
|
return *m.Exchanges
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Hello) GetCiphers() string {
|
func (m *Propose) GetCiphers() string {
|
||||||
if m != nil && m.Ciphers != nil {
|
if m != nil && m.Ciphers != nil {
|
||||||
return *m.Ciphers
|
return *m.Ciphers
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Hello) GetHashes() string {
|
func (m *Propose) GetHashes() string {
|
||||||
if m != nil && m.Hashes != nil {
|
if m != nil && m.Hashes != nil {
|
||||||
return *m.Hashes
|
return *m.Hashes
|
||||||
}
|
}
|
||||||
@ -70,8 +72,8 @@ func (m *Hello) GetHashes() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Exchange struct {
|
type Exchange struct {
|
||||||
Epubkey []byte `protobuf:"bytes,1,req,name=epubkey" json:"epubkey,omitempty"`
|
Epubkey []byte `protobuf:"bytes,1,opt,name=epubkey" json:"epubkey,omitempty"`
|
||||||
Signature []byte `protobuf:"bytes,2,req,name=signature" json:"signature,omitempty"`
|
Signature []byte `protobuf:"bytes,2,opt,name=signature" json:"signature,omitempty"`
|
||||||
XXX_unrecognized []byte `json:"-"`
|
XXX_unrecognized []byte `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
14
crypto/spipe/message.proto
Normal file
14
crypto/spipe/message.proto
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package spipe;
|
||||||
|
|
||||||
|
message Propose {
|
||||||
|
optional bytes rand = 1;
|
||||||
|
optional bytes pubkey = 2;
|
||||||
|
optional string exchanges = 3;
|
||||||
|
optional string ciphers = 4;
|
||||||
|
optional string hashes = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Exchange {
|
||||||
|
optional bytes epubkey = 1;
|
||||||
|
optional bytes signature = 2;
|
||||||
|
}
|
76
crypto/spipe/pipe.go
Normal file
76
crypto/spipe/pipe.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package spipe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
context "github.com/jbenet/go-ipfs/Godeps/_workspace/src/code.google.com/p/go.net/context"
|
||||||
|
peer "github.com/jbenet/go-ipfs/peer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Duplex is a simple duplex channel
|
||||||
|
type Duplex struct {
|
||||||
|
In chan []byte
|
||||||
|
Out chan []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecurePipe objects represent a bi-directional message channel.
|
||||||
|
type SecurePipe struct {
|
||||||
|
Duplex
|
||||||
|
insecure Duplex
|
||||||
|
|
||||||
|
local *peer.Peer
|
||||||
|
remote *peer.Peer
|
||||||
|
|
||||||
|
params params
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// options in a secure pipe
|
||||||
|
type params struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSecurePipe constructs a pipe with channels of a given buffer size.
|
||||||
|
func NewSecurePipe(ctx context.Context, bufsize int, local,
|
||||||
|
remote *peer.Peer) (*SecurePipe, error) {
|
||||||
|
|
||||||
|
sp := &SecurePipe{
|
||||||
|
Duplex: Duplex{
|
||||||
|
In: make(chan []byte, bufsize),
|
||||||
|
Out: make(chan []byte, bufsize),
|
||||||
|
},
|
||||||
|
local: local,
|
||||||
|
remote: remote,
|
||||||
|
}
|
||||||
|
return sp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap creates a secure connection on top of an insecure duplex channel.
|
||||||
|
func (s *SecurePipe) Wrap(ctx context.Context, insecure Duplex) error {
|
||||||
|
if s.ctx != nil {
|
||||||
|
return errors.New("Pipe in use")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.insecure = insecure
|
||||||
|
s.ctx, s.cancel = context.WithCancel(ctx)
|
||||||
|
|
||||||
|
if err := s.handshake(); err != nil {
|
||||||
|
s.cancel()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the secure pipe
|
||||||
|
func (s *SecurePipe) Close() error {
|
||||||
|
if s.cancel == nil {
|
||||||
|
return errors.New("pipe already closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.cancel()
|
||||||
|
s.cancel = nil
|
||||||
|
close(s.In)
|
||||||
|
return nil
|
||||||
|
}
|
@ -1,14 +0,0 @@
|
|||||||
package identify;
|
|
||||||
|
|
||||||
message Hello {
|
|
||||||
required bytes rand = 1;
|
|
||||||
required bytes pubkey = 2;
|
|
||||||
required string exchanges = 3;
|
|
||||||
required string ciphers = 4;
|
|
||||||
required string hashes = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Exchange {
|
|
||||||
required bytes epubkey = 1;
|
|
||||||
required bytes signature = 2;
|
|
||||||
}
|
|
Reference in New Issue
Block a user