diff --git a/crypto/spipe/Makefile b/crypto/spipe/Makefile new file mode 100644 index 000000000..7e737b6d8 --- /dev/null +++ b/crypto/spipe/Makefile @@ -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 diff --git a/crypto/spipe/handshake.go b/crypto/spipe/handshake.go new file mode 100644 index 000000000..8c09aff7a --- /dev/null +++ b/crypto/spipe/handshake.go @@ -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!") +} diff --git a/identify/identify_test.go b/crypto/spipe/identify_test.go similarity index 97% rename from identify/identify_test.go rename to crypto/spipe/identify_test.go index 3d529f3e4..210d0cfcc 100644 --- a/identify/identify_test.go +++ b/crypto/spipe/identify_test.go @@ -1,4 +1,4 @@ -package identify +package spipe import ( "testing" diff --git a/identify/message.pb.go b/crypto/spipe/message.pb.go similarity index 50% rename from identify/message.pb.go rename to crypto/spipe/message.pb.go index bd373c6e9..1c22bfa5f 100644 --- a/identify/message.pb.go +++ b/crypto/spipe/message.pb.go @@ -1,68 +1,70 @@ -// Code generated by protoc-gen-go. +// Code generated by protoc-gen-gogo. // source: message.proto // 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: message.proto It has these top-level messages: - Hello + Propose 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" -// 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 _ = &json.SyntaxError{} var _ = math.Inf -type Hello struct { - Rand []byte `protobuf:"bytes,1,req,name=rand" json:"rand,omitempty"` - Pubkey []byte `protobuf:"bytes,2,req,name=pubkey" json:"pubkey,omitempty"` - Exchanges *string `protobuf:"bytes,3,req,name=exchanges" json:"exchanges,omitempty"` - Ciphers *string `protobuf:"bytes,4,req,name=ciphers" json:"ciphers,omitempty"` - Hashes *string `protobuf:"bytes,5,req,name=hashes" json:"hashes,omitempty"` +type Propose struct { + Rand []byte `protobuf:"bytes,1,opt,name=rand" json:"rand,omitempty"` + Pubkey []byte `protobuf:"bytes,2,opt,name=pubkey" json:"pubkey,omitempty"` + Exchanges *string `protobuf:"bytes,3,opt,name=exchanges" json:"exchanges,omitempty"` + Ciphers *string `protobuf:"bytes,4,opt,name=ciphers" json:"ciphers,omitempty"` + Hashes *string `protobuf:"bytes,5,opt,name=hashes" json:"hashes,omitempty"` XXX_unrecognized []byte `json:"-"` } -func (m *Hello) Reset() { *m = Hello{} } -func (m *Hello) String() string { return proto.CompactTextString(m) } -func (*Hello) ProtoMessage() {} +func (m *Propose) Reset() { *m = Propose{} } +func (m *Propose) String() string { return proto.CompactTextString(m) } +func (*Propose) ProtoMessage() {} -func (m *Hello) GetRand() []byte { +func (m *Propose) GetRand() []byte { if m != nil { return m.Rand } return nil } -func (m *Hello) GetPubkey() []byte { +func (m *Propose) GetPubkey() []byte { if m != nil { return m.Pubkey } return nil } -func (m *Hello) GetExchanges() string { +func (m *Propose) GetExchanges() string { if m != nil && m.Exchanges != nil { return *m.Exchanges } return "" } -func (m *Hello) GetCiphers() string { +func (m *Propose) GetCiphers() string { if m != nil && m.Ciphers != nil { return *m.Ciphers } return "" } -func (m *Hello) GetHashes() string { +func (m *Propose) GetHashes() string { if m != nil && m.Hashes != nil { return *m.Hashes } @@ -70,8 +72,8 @@ func (m *Hello) GetHashes() string { } type Exchange struct { - Epubkey []byte `protobuf:"bytes,1,req,name=epubkey" json:"epubkey,omitempty"` - Signature []byte `protobuf:"bytes,2,req,name=signature" json:"signature,omitempty"` + Epubkey []byte `protobuf:"bytes,1,opt,name=epubkey" json:"epubkey,omitempty"` + Signature []byte `protobuf:"bytes,2,opt,name=signature" json:"signature,omitempty"` XXX_unrecognized []byte `json:"-"` } diff --git a/crypto/spipe/message.proto b/crypto/spipe/message.proto new file mode 100644 index 000000000..191dd0b80 --- /dev/null +++ b/crypto/spipe/message.proto @@ -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; +} diff --git a/crypto/spipe/pipe.go b/crypto/spipe/pipe.go new file mode 100644 index 000000000..caa539275 --- /dev/null +++ b/crypto/spipe/pipe.go @@ -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 +} diff --git a/identify/message.proto b/identify/message.proto deleted file mode 100644 index 4c3e032e5..000000000 --- a/identify/message.proto +++ /dev/null @@ -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; -}