gosecure

package module
v1.5.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 11, 2020 License: MIT Imports: 13 Imported by: 0

README

Go Secure

Build Issues Pull Requests Go Doc License

gosecure provides various functions designed to sit on top of, and compliment the [Gorilla Toolkit][]. Secure (bcrypt) hashing functions, secure endpoints via JWT, rate limiters on endpoints, constant time execution, and safe logging of core HTTP conversations are provided.

Installation

go get -u bitbucket.org/idomdavis/gosecure

Usage

A simple secure API example can be found under example/gosecure.go. You can use:

make example

To run the example on port 7474. Interaction with the API can be performed using:

curl "http://localhost:7474/login" \
     -u 'guest:password'
curl "http://localhost:7474/endpoint" \
     -H 'Authorization: Bearer ...'

Where the bearer string is the output from the call to login.

Documentation

Overview

Package gosecure provides hashing, checking and associated functions for passphrases and JWT tokens. Tokens allow for session data to be passed in by a client having been encoded earlier. With both hashes and tokens secure cryptographic functions are used. All functions can be called in a time boxed fashion with guarantees about minimum execution time using the Timebox function.

Index

Examples

Constants

View Source
const (
	ContentType  = "Content-Type"
	ContentPlain = "text/plain;charset=utf-8"
	ContentJSON  = "application/json;charset=UTF-8"
	ContentHTML  = "text/html;charset=utf-8"
)

Content types.

View Source
const AuthHeader = "Authorization"

AuthHeader is the header used to store the bearer token.

View Source
const SessionKey = "session"

SessionKey is used to store a session in a Gorilla context.

Variables

View Source
var (
	// ErrEmptyPassphrase is returned if an attempt is made to hash an empty
	// passphrase.
	ErrEmptyPassphrase = errors.New("security: passphrase cannot be empty")

	// ErrEmptyHash is returned if an attempt is made to compare a passphrase
	// against an empty hash slice.
	ErrEmptyHash = errors.New("security: hash cannot be empty")

	// ErrMismatchedPassphrase is returned if the passphrase does not match the
	// provided hash.
	ErrMismatchedPassphrase = errors.New("security: passphrase does not match hash")
)
View Source
var DefaultAlgorithm = Bcrypt{Cost: bcrypt.DefaultCost}

DefaultAlgorithm is the algorithm used by Hash and Compare.

View Source
var ErrNoCredentialsFound = errors.New("no credentials found")

ErrNoCredentialsFound is returned if an attempt is made to validate an empty bearer token.

View Source
var Fuse = 100000

Fuse is used to limit the number of hits stored against an action. If the fuse is trigger the action is thrown away. While it's theoretically possible to game the limited by carefully constructing the number of hits vs the limit and TTL in reality the Fuse will only be blown when a DOS style attack is happening and should be well over the limits set on the limiter.

Theoretical max memory usage is Fuse * size * ~60bytes.

Functions

func Compare

func Compare(passphrase string, hash []byte) error

Compare a passphrase to a previously hashed passphrase using the default crypto algorithm. If the passphrase matches the hash then this function will return nil. A mismatched passphrase will result in ErrMismatchedPassphrase. All other errors indicate something went wrong comparing the hash and passphrase.

Example
package main

import (
	"fmt"

	"bitbucket.org/idomdavis/gosecure"
)

func main() {
	b := []byte{
		36, 50, 97, 36, 49, 48, 36, 100, 117, 66, 70, 85, 103, 120, 51, 68,
		80, 106, 103, 108, 90, 74, 84, 51, 101, 56, 57, 122, 79, 87, 122,
		74, 122, 51, 46, 108, 82, 73, 106, 56, 115, 101, 88, 112, 85, 46,
		68, 67, 112, 78, 48, 121, 67, 97, 57, 109, 74, 98, 114, 67}

	if err := gosecure.Compare("passphrase", b); err != nil {
		fmt.Println("invalid passphrase")
	} else {
		fmt.Println("passphrase accepted")
	}

}
Output:

passphrase accepted

func Hash

func Hash(passphrase string) ([]byte, error)

Hash a passphrase using the default crypto algorithm.

Example
package main

import (
	"fmt"

	"bitbucket.org/idomdavis/gosecure"
)

func main() {
	if h, err := gosecure.Hash("passphrase"); err == nil {
		fmt.Println(len(h))
	}

}
Output:

60

func Timebox

func Timebox(duration time.Duration, task func())

Timebox will run the provided function, taking at least the given duration.

Example
package main

import (
	"fmt"
	"os"
	"time"

	"bitbucket.org/idomdavis/gosecure"
)

func main() {
	// Duration wants to be longer than the expected maximum of the provided
	// function.
	duration := time.Millisecond * 50
	start := time.Now()

	// Use closures to send and retrieve values
	var err error
	msg := "running..."

	gosecure.Timebox(duration, func() {
		_, err = fmt.Fprintln(os.Stdout, msg)
	})

	if err != nil {
		fmt.Printf("unexpected error running timeboxed function: %s\n", err)
	}

	end := time.Now()
	if (end.Sub(start)) > duration {
		fmt.Println("time boxed")
	}

}
Output:

running...
time boxed

Types

type API added in v1.1.0

type API interface {

	// RegisterSecure registers a secure endpoint on the given url for the given
	// method.
	RegisterSecure(method, url string, handler http.HandlerFunc)

	// RegisterInsecure registers an insecure endpoint on the given url for the
	// given method.
	RegisterInsecure(method, url string, handler http.HandlerFunc)

	// Router returns a Gorilla router for the currently registered endpoints.
	// The optional set of middleware functions are applied to the secure router
	// that forms part of the router before it's returned.
	Router(signatory Signatory, f ...mux.MiddlewareFunc) *mux.Router

	// Handler returns an http.Handler for the currently registered endpoints.
	Handler(signatory Signatory) http.Handler
}

API provides a method of building an http.Handler with secure and insecure endpoints.

func NewAPI added in v1.1.0

func NewAPI(source, user RateLimiter) API

NewAPI returns a new API type that uses Gorilla for it's underlying router. All patterns and rules that apply to Gorilla paths can be used here.

The limiters are applied to all routes, however the user limiter ignores any calls not setting an HTTP Basic Auth user.

Secure routes must have an Authorization header and must have a valid bearer token. Calling a secure route without the Authorization header will result in a 404 being returned. Calling a secure route with an invalid token will result in a 403 being returned.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"net/url"
	"time"

	"bitbucket.org/idomdavis/gosecure"
)

func main() {
	const (
		size  = 1000
		limit = 100
		ttl   = time.Minute
	)

	source := gosecure.NewSourceLimiter(gosecure.NewLimiter(size, limit, ttl))
	user := gosecure.NewUserLimiter(gosecure.NewLimiter(size, limit, ttl))

	api := gosecure.NewAPI(source, user)

	signatory := gosecure.NewSignatory("secret", time.Hour)

	api.RegisterInsecure("GET", "/login",
		func(w http.ResponseWriter, _ *http.Request) {
			// Login logic here, bailing if we've not logged in
			fmt.Println("Logged in!")

			token, _ := signatory.Generate(gosecure.Session{})

			w.WriteHeader(http.StatusOK)
			_, _ = w.Write([]byte(token))
		})

	api.RegisterSecure("GET", "/endpoint",
		func(w http.ResponseWriter, _ *http.Request) {
			w.WriteHeader(http.StatusOK)
			_, _ = w.Write([]byte("Logged in and verified!"))
		})

	handler := api.Handler(signatory)

	// Simulate a call to the handler
	handler.ServeHTTP(httptest.NewRecorder(), &http.Request{
		Method: "GET", URL: &url.URL{Path: "/login"},
	})

}
Output:

Logged in!

type Authenticator

type Authenticator interface {
	// Authenticate returns a handler function used to authenticate requests.
	Authenticate(next http.Handler) http.Handler
}

Authenticator types provide middleware that will authenticate requests.

func NewAuthenticator

func NewAuthenticator(signatory Signatory) Authenticator

NewAuthenticator returns a type that can be used for authentication middleware.

type Bcrypt

type Bcrypt struct {
	Cost int
}

Bcrypt hashing algorithm. Should be all you need.

func (Bcrypt) Compare

func (Bcrypt) Compare(passphrase string, hash []byte) error

Compare a passphrase to a previously hashed passphrase. If the passphrase matches the hash then this function will return nil. A mismatched passphrase will result in ErrMismatchedPassphrase. All other errors indicate something went wrong comparing the hash and passphrase.

func (Bcrypt) Hash

func (b Bcrypt) Hash(passphrase string) ([]byte, error)

Hash a passphrase. If the passphrase is empty an error will be returned.

type Conversation added in v1.1.0

type Conversation interface {
	// Respond to the conversation, using the default response for the given
	// code. Respond will close the conversation. Calling Respond on a closed
	// conversation will do nothing.
	Respond(code int)

	// Reply to the conversation with an HTTP OK and the given body. Content
	// type is determined based on the given body. A string/byte slice starting
	// with '<' will be treated as HTML, otherwise it is treated as plain text.
	// All other types are treated as JSON. Reply will close the conversation.
	// Calling Reply on a closed conversation will do nothing.
	Reply(body interface{})

	// Response allows full control over the conversations response setting both
	// the body and HTTP response code. The body will either be in plain text
	// for []byte or string bodies, or JSON for all other types. Response will
	// close the conversation. Calling Response on a closed conversation will
	// do nothing.
	Response(code int, body interface{})
}

Conversation provides helper functions when dealing with HTTP requests, gracefully handling errors to provide information without leaking unwanted data.

func NewConversation added in v1.1.0

func NewConversation(w http.ResponseWriter, r *http.Request) Conversation

NewConversation builds a new conversation for the given request and response, populating some basic headers. The conversation will use the DefaultListener set in the log package to report errors and warnings, throwing these away if DefaultListener is nil. The conversation will report the request method, URL, and/or remote address where appropriate. No other request data is reported.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"net/url"

	"bitbucket.org/idomdavis/gosecure"
	"bitbucket.org/idomdavis/gosecure/log"
)

func main() {
	// Temporarily set the error handler to the console error listener
	listener := log.DefaultListener
	recorder := &log.Recorder{}
	log.DefaultListener = recorder

	defer func() { log.DefaultListener = listener }()

	c := gosecure.NewConversation(httptest.NewRecorder(), &http.Request{
		Method:     "GET",
		URL:        &url.URL{Path: "/url"},
		RemoteAddr: "remote",
	})

	c.Reply("Called")

	// Will warn about a call to a closed endpoint
	c.Respond(200)

	fmt.Println(recorder.Output)

}
Output:

warn Attempt to use a closed conversation map[code:200 endpoint:/url method:GET type:Response]

type Crypto

type Crypto interface {
	Hash(passphrase string) ([]byte, error)
	Compare(passphrase string, hash []byte) error
}

Crypto defines a type that can cryptographically hash and compare a passphrase. Implementations of Crypto want to implement strong hashing functions such as bcrypt. Implementing this for things like MD5 is a stunningly bad idea.

type Limiter

type Limiter interface {
	// Limit checks if the action provided should be limited, returning true if
	// it should.
	Limit(action string) bool
}

A Limiter is used to indicate if an action has occurred too often and should be limited.

func NewLimiter

func NewLimiter(size, limit int, ttl time.Duration) Limiter

NewLimiter returns a limiter that uses a lazy LRU backing cache. The limiter prunes expired hits for an action on a call to Limit, and will drop the least recently used item from the cache if it exceeds the defined size.

TTL sizes want to be small (in the 1 second to 1 minute range) to avoid the number of hits in the cache getting too large in case of a DOS style attack. A Fuse value is used to prevent runaway memory consumption in this case.

type RateLimiter

type RateLimiter interface {

	// Limit returns a handler function used to rate limit requests.
	Limit(next http.Handler) http.Handler
}

RateLimiter defines Gorilla middleware to be used to rate limit requests. If a limiter is triggered it returns "429 Too Many Requests".

func NewSourceLimiter

func NewSourceLimiter(limiter Limiter) RateLimiter

NewSourceLimiter returns middleware that will limit requests based on the remote address, or the source of the request.

If this limiter is triggered a warning log event is emitted.

func NewUserLimiter

func NewUserLimiter(limiter Limiter) RateLimiter

NewUserLimiter returns middleware that will limit requests based on the user in the basic auth header. If no user is set this limiter is skipped.

If this limiter is triggered a warning log event is emitted.

type Session

type Session map[string]string

Session stores session claims encoded in the JWT.

type Signatory

type Signatory interface {
	// Generate a bearer token, embedding the given Session in the token.
	Generate(data Session) (string, error)

	// Validate a bearer token, returning the associated Session embedded in the
	// token. An error is returned if there is a problem validating the token.
	Validate(bearer string) (Session, error)
}

A Signatory is used to sign and validate sessions via the use of JWT bearer tokens.

func NewSignatory

func NewSignatory(secret string, ttl time.Duration) Signatory

NewSignatory constructs a signatory using the given secret string. If tokens are going to be shared between signatories they must use the same secret.

Example
package main

import (
	"fmt"
	"time"

	"bitbucket.org/idomdavis/gosecure"
)

const secret = "secretString"

func main() {
	// Use the default TTL
	_ = gosecure.NewSignatory(secret, 0)

	// Specify TTL, generate, then validate a token.
	sig := gosecure.NewSignatory(secret, time.Hour)
	token, _ := sig.Generate(gosecure.Session{"key": "value"})
	session, _ := sig.Validate(token)

	fmt.Println(session["key"])

}
Output:

value

Directories

Path Synopsis
Package log provides wrappers around logging for gosecure.
Package log provides wrappers around logging for gosecure.
Package mock provides mock handlers for testing.
Package mock provides mock handlers for testing.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL