Pkg.go.dev is a new destination for Go discovery & docs. Check it out at pkg.go.dev/gopkg.in/orus-io/natscrypto.v0 and share your feedback.

package natscrypto

import "gopkg.in/orus-io/natscrypto.v0"

Package natscrypto provides a PKI encryption layer on top of nats.Conn, as well a port of nats.EncodedConn on top of it.

Once connected to a nats server, any client can subscribe and publish to any subject. When the nats server is shared among entities that should not be able to eavesdrop on each other, it can be a problem.

Our approach is to encrypt and sign each message with openpgp, so the actors can keep information private and certify the origin of the message.

This package is our implementation of this approach. The openpgp encrytion is the only provided one, but adding one would be pretty straighforward.

Basic usage ¶

Once a connection is established with the nats server, we can wrap it in a natscrypto connection. Any message sent through this wrapper will be encrypted for the desired recipients, and any incoming message will be first decrypted and its signer verified

First, we need to setup an encrypter that hold our keyring:

var ( publicEntities openpgp.EntityList privateEntity *openpgp.Entity ) // Init the encrypter encrypter := natscrypto.NewPGPEncrypter(publicEntities...) encrypter.AddEntity(privateEntity) // myidentity is my private key fingerprint. Only the encrypter // needs to handle the actual keys, the rest of natscrypto only // manipulates string ids. Their exact signification depends on // the encrypter myidentity := string(privateEntity.PrimaryKey.FingerPrint[:20]) // Get the identity of the potential recipients for future use rec1 := string(publicEntities[0].PrimaryKey.FingerPrint[:20]) rec2 := string(publicEntities[1].PrimaryKey.FingerPrint[:20]) rec3 := string(publicEntities[2].PrimaryKey.FingerPrint[:20])

Then we can wrap the connection:

conn := nats.Connect(...) eConn := natscrypto.NewConn(conn, myidentity, encrypter)

Post a message:

// Declare for which identities messages sent to "test" should be // encrypted eConn.SetSubjectRecipients("test", rec1) // The message ("hello") will be signed using privateEntity eConn.Publish("test", []byte("hello")) // We can publish for arbitrary recipients on a single call eConn.PublishFor("test", []byte("hello"), rec2, rec3)

Subscribe:

// any known emitter will be accepted on this subscription sub, err := eConn.SubscribeSync("incoming") // only rec1 will be accepted on this one sub, err := eConn.Subscribe("other", func(*natscrypto Msg) {}, rec1)

The received messages have 3 extra attributes in addition to a classic nats.Msg:

- Signer: the id of the verified signer, or empty. - Recipients: the ids of the recipients (only one when receiving, but could be more when emitting). - Error: in some cases we can get messages that could be decrypted but have a signer problem. In a error handler, the 'Error' attribute of the message will be set so we can handle unknown signers gracefully (for example)

EncodedConn are the preferred way to interface with NATS. They wrap a bare connection to a nats server and have an extendable encoder system that will encode and decode messages from raw Go types.

Since nats.EncodedConn cannot work on top of a natscrypto.Conn, we ported it. natscrypto.EncodedConn has both the features of natscrypto.Conn and nats.EncodedConn.

doc.go enc.go natscrypto.go pgp.go

❖ const NoReply = ""

NoReply can be used as the "reply" argument when no reply is needed

❖ var ( // ErrNilConnection A nil connection was passed to a function expecting a non-nil one ErrNilConnection = errors.New("Nil Connection") // ErrNilEncrypter A nil encrypter was passed to a function expecting a non-nil one ErrNilEncrypter = errors.New("Nil Encrypter") // ErrNoRecipient An empty list of recipients was passed. ErrNoRecipient = errors.New("No Recipient. An empty list of recipient was passed.") // ErrUnknownSigner is returned by DecryptData is the message is signed by an // unknown identity ErrUnknownSigner = errors.New("Unknown Signer") // ErrUnsignedMessage is returned by DecryptData if the message is not pgp signed ErrUnsignedMessage = errors.New("Unsigned Message") // ErrSignerNotAuthorized is set on Msg when the signer is not authorized on a // subscription ErrSignerNotAuthorized = errors.New("natscrypto: Signer not authorized") )

❖ var ( // ErrNilEntity is returned or panicked by functions expecting a non-nil // *openpgp.Entity ErrNilEntity = errors.New("Nil Entity") )

❖ var ErrNonEncryptedResponse = errors.New("Non encrypted Response")

ErrNonEncryptedResponse is returned by PublishUnencrypted if the response is not encrypted.

❖ type Conn struct { *nats.Conn Encrypter Encrypter Identity string SubjectRecipients map[string][]string ReplyRecipients map[string]replyRecipient // contains filtered or unexported fields }

Conn is a nats connection on which every message sent is encrypted for recipients of the subject, and every message received is decrypted automatically

❖ func NewConn(c *nats.Conn, identity string, encrypter Encrypter) (*Conn, error)

NewConn wraps a nats.Conn in a Conn that uses the passed encrypter A same nats.Conn can be share among several natscrypto.Conn.

❖ func (c *Conn) ChanQueueSubscribe(subject, group string, ch chan *Msg, signers ...string) (*Subscription, error)

ChanQueueSubscribe will place all messages received on the channel. You should not close the channel until sub.Unsubscribe() has been called.

❖ func (c *Conn) ChanSubscribe(subject string, ch chan *Msg, signers ...string) (*Subscription, error)

ChanSubscribe will place all messages received on the channel. You should not close the channel until sub.Unsubscribe() has been called.

Close closes the encrypted connection, _not_ the underlying nats.Conn. To close both the encryption layer and the actual nats.Conn, use CloseAll()

CloseAll closes the encrypted connection _and_ the underlying nats.Conn

GetRecipients returns the default recipients for a given subject.

❖ func (c *Conn) Publish(subject string, data []byte) error

Publish publishes the data argument to the given subject. The data argument will be encrypted for the destination identities of the subject

❖ func (c *Conn) PublishFor(subject string, data []byte, recipients ...string) error

PublishFor publishes to a subject for specific recipients

PublishMsg publishes the Msg structure, which includes the Subject, an optional Reply and an optional Data field.

❖ func (c *Conn) PublishRequest(subj, reply string, data []byte) error

PublishRequest will perform a Publish() excpecting a response on the reply subject. Use Request() for automatically waiting for a response inline. In this specific version of PublishRequest, the 'reply' gets encrypted too

❖ func (c *Conn) PublishRequestFor(subj, reply string, data []byte, recipients ...string) error

PublishRequestFor is PublishRequest with explicit recipients

❖ func (c *Conn) QueueSubscribe(subject, queue string, cb MsgHandler, signers ...string) (*Subscription, error)

QueueSubscribe will create a queue subscription on the given subject and process incoming messages using the specified Handler.

❖ func (c *Conn) Request(subj string, data []byte, timeout time.Duration) (*Msg, error)

Request will create an Inbox and perform a Request() call with the Inbox reply for the data v. A response will be decrypted. This implementation is copied from nats.Conn.Request, but using our own PublishRequest that will encrypt the reply, and our own Subscription that will decrypt the incoming message

❖ func (c *Conn) RequestFor(subj string, data []byte, timeout time.Duration, recipients ...string) (*Msg, error)

RequestFor is Request with explicit recipients

SetDefaultDecryptErrorHandler sets the default decrypt error handler of all the subscriptions to come. Already created subscriptions will be untouched

SetMultiSubjectRecipients associates recipients to subjects

❖ func (c *Conn) SetSubjectRecipients(subject string, recipients []string)

SetSubjectRecipients associates a list of recipients to a subject if subject is "", the recipients are used as default for subjects having no explicit recipients

❖ func (c *Conn) Subscribe(subject string, cb MsgHandler, signers ...string) (*Subscription, error)

Subscribe will create a subscription on the given subject and process incoming messages using the specified Handler. The Handler should be a func that matches a signature from the description of Handler from above. signers is an optional list of authorized signers

❖ func (c *Conn) SubscribeSync(subj string, signers ...string) (*Subscription, error)

SubscribeSync is syntactic sugar for Subscribe(subject, nil).

❖ type DecryptErrorHandler func(sub *Subscription, msg *Msg) *Msg

DecryptErrorHandler are callbacks for decryption errors if the function returns a nil Msg, the message will stop its course and never make it down the to final handler.

❖ type EncodedConn struct { *Conn Enc nats.Encoder }

EncodedConn is a Conn with encoding/decoding capabilities

❖ func NewEncodedConn(c *Conn, encType string) (*EncodedConn, error)

NewEncodedConn wraps a Conn with encoding/decoding utilities

❖ func (c *EncodedConn) Publish(subject string, v interface{}) error

Publish publishes the data argument to the given subject. The data argument will be encoded using the associated encoder.

❖ func (c *EncodedConn) PublishFor(subject string, v interface{}, recipients ...string) error

PublishFor same as Publish for a specific recipient

❖ func (c *EncodedConn) PublishRequest(subject, reply string, v interface{}) error

PublishRequest will perform a Publish() expecting a response on the reply subject. Use Request() for automatically waiting for a response inline.

❖ func (c *EncodedConn) PublishRequestFor(subject, reply string, v interface{}, recipients ...string) error

PublishRequestFor same as PublishRequest for specific recipients

❖ func (c *EncodedConn) PublishRequestUnencrypted(subject, reply string, v interface{}) error

PublishRequestUnencrypted publishes the data encoded only, not encrypted.

❖ func (c *EncodedConn) PublishUnencrypted(subject string, v interface{}) error

PublishUnencrypted publishes the data encoded only, not encrypted

❖ func (c *EncodedConn) QueueSubscribe(subject, queue string, cb nats.Handler) (*EncodedSubscription, error)

QueueSubscribe will create a queue subscription on the given subject and process incoming messages using the specified Handler. The Handler should be a func that matches a signature from the description of Handler from above.

❖ func (c *EncodedConn) Request(subject string, v interface{}, vPtr interface{}, timeout time.Duration) error

Request will create an Inbox and perform a Request() call with the Inbox reply for the data v. A response will be decoded into the vPtrResponse.

❖ func (c *EncodedConn) RequestFor(subject string, v interface{}, vPtr interface{}, timeout time.Duration, recipients ...string) error

RequestFor same as Request for specific recipients

❖ func (c *EncodedConn) RequestUnencrypted(subject string, v interface{}, vPtr interface{}, timeout time.Duration) (encrypted bool, err error)

RequestUnencrypted same as Request but the emitted message will _not_ be encrypted. The reponse may be encrypted though, in which case it is transparenly decrypted, and the returned bool is 'true'

❖ func (c *EncodedConn) RequestUnsafe(subject string, v interface{}, vPtr interface{}, timeout time.Duration) (encrypted bool, err error)

RequestUnsafe same as Request but if the response is not encryted or signed, the message will be decoded anyway

❖ func (c *EncodedConn) Subscribe(subject string, cb nats.Handler) (*EncodedSubscription, error)

Subscribe will create a subscription on the given subject and process incoming messages using the specified Handler. The Handler should be a func that matches a signature from the description of Handler from above.

SubscribeSync is syntactic sugar for Subscribe(subject, nil).

❖ type EncodedSubscription struct { *Subscription Enc nats.Encoder }

EncodedSubscription wraps a Conn and add a Next() function That decode incoming messages

❖ func (s EncodedSubscription) Next(vPtr interface{}, timeout time.Duration) error

Next decodes the next message available to a synchronous subscriber or block until one is available.

❖ func (s EncodedSubscription) NextSubject(subject *string, vPtr interface{}, timeout time.Duration) error

NextSubject decodes the next message available to a synchronous subscriber or block until one is available.

❖ func (s EncodedSubscription) NextSubjectReply(subject, reply *string, vPtr interface{}, timeout time.Duration) error

NextSubjectReply decodes the next message available to a synchronous subscriber or block until one is available.

❖ type Encrypter interface { EncryptData (data []byte, recipients []string, signer string) ([]byte, error) DecryptData (data []byte) (cleardata []byte, recipients []string, signer string, err error) }

Encrypter is implemented by message encrypters Both function should be routine-safe as they may be called in parallel routines

❖ type Msg struct { *nats.Msg Signer string Recipients []string Error error }

Msg is a wrapper for nats.Msg with added Signer and Recipients There fields are filled by decryption or by the user for proper encryption The identities can be any string that the encoder will recognize as a unique identity, generally a fingerprint

❖ func NewMsg(subject string, data []byte, sender string, recipients ...string) *Msg

NewMsg initialize a Msg

❖ type MsgHandler func(msg *Msg)

MsgHandler is a callback function that processes messages delived to asynchronous subscribers

❖ type PGPEncrypter struct { Identities map[string]*openpgp.Entity AllEntities openpgp.EntityList OneShotEntities map[string]*openpgp.Entity // contains filtered or unexported fields }

PGPEncrypter is a openpgp based Encrypter for EncryptedConn

NewPGPEncrypter initialize a PGPEncrypter

AddEntity add one of more openpgp entities to the encrypter. If the entity contains a private key, it is added to the PrivateIdentities too, which means the PGPEncrypter will be able to decrypt messages to it Panics if the entity is nil or has no PrimaryKey

AddOneShotEntity add an entity that can be used only once for encrypting only (not for verification)

❖ func (e *PGPEncrypter) DecryptData(data []byte) (cleardata []byte, recipients []string, signer string, err error)

DecryptData decrypt the data and extract the recipients and signer

❖ func (e *PGPEncrypter) EncryptData(data []byte, recipients []string, signer string) ([]byte, error)

EncryptData encrypt the data with the recipients public keys and sign it sith signer private key

RemoveID removes an entity from the encrypter given its fingerprint

❖ type Subscription struct { *nats.Subscription Conn *Conn // contains filtered or unexported fields }

Subscription wraps nats.Subscription and override its 'NextMsg' function it also provides callbacks on decryption errors, so a subscriber can handle such errors or even reply to badly or unsigned requests. The default error handler will drop the message so the final handler never sees it.

NextMsg returns the next message available to a synchronous subscriber of block until one is available. Badly encrypted incoming messages will return an error

SetAuthorizedSigners changes the list of signers allowed on this subscription. Any message received from a signer outside this list will be stopped and handled as error

SetDecryptErrorHandler sets a callback that is called when a decryption error occurs. The handler can: - return a Msg, possibly the original one. In this case, the message will

be passed down to the final subscriptor (cb, sync or chan)

- return nil, which will make the message disappear and never reach the final

handler. NextMsg() will however return the original Msg.Error as an error

Unsubscribe will remove interest in the given subject.