ghoststring

package module
v0.5.1 Latest Latest
Warning

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

Go to latest
Published: Sep 2, 2022 License: MIT Imports: 18 Imported by: 0

README

ghoststring

Go Reference

A Go string wrapper type that is encrypted when stringified or JSONified. The following standard library interfaces are fulfilled:

  • json.Marshaler
  • json.Unmarshaler
  • fmt.Stringer
  • fmt.GoStringer
  • encoding.TextMarshaler
  • encoding.TextUnmarshaler
  • encoding.BinaryMarshaler
  • encoding.BinaryUnmarshaler

usage

Typical usage assumes at least two systems that share a key. For example, two or more web services may not directly communicate, but are passed data via state transfer through a browser session to which they both need access without leaking to the browser.

Each instance of GhostString requires a non-empty namespace which must match a registered Ghostifyer, e.g.:

Declare a type that uses a GhostString as a field:

type Message struct {
	Recipient string                  `json:"recipient"`
	Content   ghoststring.GhostString `json:"content"`
	Mood      ghoststring.GhostString `json:"mood"`
}

Load a secret key from somewhere that meets your security requirements, such as from a mounted kubernetes secret:

secretKeyBytes, err := os.ReadFile("/path/to/secret-key")
if err != nil {
	return err
}

Create a Ghostifyer with the secret key and namespace in use:

gh, err := ghoststring.NewAES256GCMSingleKeyGhostifyer("heck.example.org", string(secretKeyBytes))
if err != nil {
	return err
}

Register the Ghostifyer:

if err := ghoststring.SetGhostifyer(gh); err != nil {
	return err
}

Use the declared type as with any other struct type:

msg := &Message{
	Recipient: "morningstar@heck.example.org",
	Content: ghoststring.GhostString{
		Namespace: "heck.example.org",
		Str:       "We meet me at the fjord at dawn. Bring donuts, please.",
	},
	Mood: ghoststring.GhostString{
		Namespace: "heck.example.org",
		Str:       "giddy",
	},
}

When the type instance is encoded to JSON, the GhostString fields will automatically encode to a JSON string type:

{
  "recipient": "morningstar@heck.example.org",
  "content": "👻:NTNjZjdmMWZjNzU2NTY1ODFkN2E4ZDI4aGVjay5leGFtcGxlLm9yZzo60XHqdSEbswpPbKLrDUuBZCrtUQLjmJ1NxHsqHMgBJjrB10O7JC4rwiMZw+wf/BOJGmu9ZCpMjgMpu18/VgDBpLn4n1nBNw==",
  "mood": "👻:YmYzZDVjNDVhNTUyMTA1Mzg0ZWU0NjI0aGVjay5leGFtcGxlLm9yZzo6SML6iqmwSoDhX3b2SQXsMLJPMHHS"
}

In the event of an unrecognized namespace, the GhostString fields will be encoded as empty strings, e.g.:

msg := &Message{
	Recipient: "frith@heck.example.org",
	Content: ghoststring.GhostString{
		Namespace: "heck.example.org",
		Str:       "Next time I'm bringing the coffee.",
	},
	Mood: ghoststring.GhostString{
		Namespace: "wat.example.org",
		Str:       "zzz",
	},
}

Because the "wat.example.org" namespace isn't registered, the output will look like this:

{
  "recipient": "frith@heck.example.org",
  "content": "👻:NTNjZjdmMWZjNzU2NTY1ODFkN2E4ZDI4aGVjay5leGFtcGxlLm9yZzo60XHqdSEbswpPbKLrDUuBZCrtUQLjmJ1NxHsqHMgBJjrB10O7JC4rwiMZw+wf/BOJGmu9ZCpMjgMpu18/VgDBpLn4n1nBNw==",
  "mood": ""
}

Any system that needs to read the encrypted contents must decode the JSON into a type that uses GhostString for the matching fields in a process where a matching Ghostifyer has been registered. Other systems may treat the values as opaque strings.

Documentation

Index

Examples

Constants

View Source
const (
	NamespaceMatchRegexp = "^[a-zA-Z][-\\._a-zA-Z0-9]{1,254}[a-zA-Z0-9]$"
	NamespaceSeparator   = "::"
	Prefix               = "👻:"

	Nonce = 12
)
View Source
const (
	EnvKeyStoreKeyPrefix = "GHOSTSTRING_KEY_{{.Namespace}}_"
)

Variables

View Source
var (
	Err = errors.New("ghoststring error")
)

Functions

func SetGhostifyer

func SetGhostifyer(gh Ghostifyer) error

Types

type GhostString

type GhostString struct {
	Namespace string
	Str       string
}

GhostString wraps a string with a JSON marshaller that uses a namespace-scoped encrypting Ghostifyer registered via SetGhostifyer

Example
package main

import (
	"encoding/json"
	"fmt"
	"regexp"
	"strings"
	"time"

	"github.com/rstudio/ghoststring"
)

func main() {
	gh, err := ghoststring.NewAES256GCMSingleKeyGhostifyer(
		"example",
		"correct horse battery staple",
	)
	if err != nil {
		panic(err)
	}

	if err := ghoststring.SetGhostifyer(gh); err != nil {
		panic(err)
	}

	type DiaryEntry struct {
		Timestamp time.Time               `json:"timestamp"`
		Text      ghoststring.GhostString `json:"text"`
	}

	type Diary struct {
		Author  string       `json:"author"`
		Entries []DiaryEntry `json:"entries"`
	}

	enc, err := json.MarshalIndent(
		&Diary{
			Author: "Eagerly Anticipated",
			Entries: []DiaryEntry{
				{
					Timestamp: time.UnixMicro(4),
					Text: ghoststring.GhostString{
						Namespace: "example",
						Str:       "Nights without you are so dark. I pray that someday you will return my flashlight.",
					},
				},
				{
					Timestamp: time.UnixMicro(-8001),
					Text: ghoststring.GhostString{
						Namespace: "unknown",
						Str:       "We may never know.",
					},
				},
			},
		},
		"",
		"  ",
	)
	if err != nil {
		panic(err)
	}

	encString := string(enc)

	if strings.Contains(encString, "We may never know.") || strings.Contains(encString, "Nights without you") {
		panic("not ghostly enough: contains cleartext")
	}

	if !strings.Contains(encString, `"text": ""`) {
		panic("not ghostly enough: lacking empty ghoststring")
	}

	if matched, err := regexp.MatchString(`.+"text": "👻:[^"]+"`, encString); !matched || err != nil {
		panic("not ghostly enough: lacking non-empty ghoststring")
	}

	fmt.Println("no peeking")

}
Output:

no peeking

func (*GhostString) Equal

func (gs *GhostString) Equal(other *GhostString) bool

Equal compares this GhostString to another

func (*GhostString) GoString added in v0.3.0

func (gs *GhostString) GoString() string

func (*GhostString) IsValid

func (gs *GhostString) IsValid() bool

IsValid checks that the wrapped string value is non-empty and the namespace is valid

func (*GhostString) MarshalBinary added in v0.3.0

func (gs *GhostString) MarshalBinary() ([]byte, error)

MarshalBinary allows GhostString to fulfill the encoding.BinaryMarshaler interface

func (*GhostString) MarshalJSON

func (gs *GhostString) MarshalJSON() ([]byte, error)

MarshalJSON allows GhostString to fulfill the json.Marshaler interface. The lack of a namespace is considered an error.

func (*GhostString) MarshalText added in v0.3.0

func (gs *GhostString) MarshalText() ([]byte, error)

MarshalText allows GhostString to fulfill the encoding.TextMarshaler interface

func (*GhostString) String

func (gs *GhostString) String() string

func (*GhostString) UnmarshalBinary added in v0.3.0

func (gs *GhostString) UnmarshalBinary(b []byte) error

UnmarshalBinary allows GhostString to fulfill the encoding.BinaryUnmarshaler interface

func (*GhostString) UnmarshalJSON

func (gs *GhostString) UnmarshalJSON(b []byte) error

UnmarshalJSON allows GhostString to fulfill the json.Unmarshaler interface. The bytes are first unmarshaled as a string and then if non-empty are passed through an "unghostify" step. The expected structure of a marshalled GhostString is:

"{Prefix}base64({nonce}{namespace}{NamespaceSeparator}{opaque-value})"

where {nonce} has the length specified as Nonce.

func (*GhostString) UnmarshalText added in v0.3.0

func (gs *GhostString) UnmarshalText(b []byte) error

UnmarshalText allows GhostString to fulfill the encoding.TextUnmarshaler interface

type Ghostifyer

type Ghostifyer interface {
	Namespace() string
	Ghostify(*GhostString) (string, error)
	Unghostify(string) (*GhostString, error)
}

Ghostifyer encodes a GhostString into a string representation.

func NewAES256GCMMultiKeyGhostifyer added in v0.5.0

func NewAES256GCMMultiKeyGhostifyer(namespace string, keys KeyStore) Ghostifyer

NewAES256GCMMultiKeyGhostifyer creates a Ghostifyer with multiple timestamped keys that uses AES-256-GCM encryption with nonce assigned at the individual string level. The keystore.Latest will be used for encryption and any key in keystore.All may be used for decryption.

func NewAES256GCMSingleKeyGhostifyer added in v0.3.0

func NewAES256GCMSingleKeyGhostifyer(namespace, key string) (Ghostifyer, error)

NewAES256GCMSingleKeyGhostifyer creates a Ghostifyer with a single key that uses AES-256-GCM encryption with nonce assigned at the individual string level.

type KeyStore added in v0.5.0

type KeyStore interface {
	Latest(ctx context.Context) ([]byte, error)
	All(ctx context.Context) ([][]byte, error)
}

func NewKeyStore added in v0.5.0

func NewKeyStore(namespace string, keys []*TimestampedKey) (KeyStore, error)

func NewKeyStoreFromEnv added in v0.5.0

func NewKeyStoreFromEnv(namespace string, env []string) (KeyStore, error)

type TimestampedKey added in v0.5.0

type TimestampedKey struct {
	Timestamp int64  `json:"timestamp"`
	Key       string `json:"key"`
	// contains filtered or unexported fields
}

Directories

Path Synopsis
cmd
internal

Jump to

Keyboard shortcuts

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