Implementation of a file-backed persistence store.

This is a rather large change. It consists of the following changes:

+ Direct access to the keycache has been removed from the core
  package. This forces all interaction with the cache to go
  through the Cryptor, which is required for persistence. The
  Cryptor needs to know when the cache has changed, and the only
  way to do this effectively is to make the Cryptor responsible
  for managing the keycache.

+ A new persist package has been added. This provides a Store
  interface, for which two implementations are provided. The
  first is a null persister: this is used when no persistence
  is configured. The second is a file-backed persistence store.

+ The Cryptor now persists the cache every time it changes.

Additionally, a number of missing returns in a function in the core
package have been added.
This commit is contained in:
Kyle Isom
2016-07-26 17:23:45 -07:00
parent 1cf72b1f6d
commit 7c95007cda
13 changed files with 1074 additions and 48 deletions

View File

@@ -77,8 +77,18 @@ type Delegations struct {
Persist bool `json:"persist"`
// Policy contains the MSP predicate for delegation
// persistence.
Policy string `json:"policy"`
// persistence, and users contains the users allowed
// to delegate.
Policy string `json:"policy"`
Users []string `json:"users"`
// Mechanism specifies the persistence mechanism to use.
Mechanism string `json:"mechanism"`
// Location contains location information for the persistence
// mechanism, such as a file path or database connection
// string.
Location string `json:"location"`
}
// Config contains all the configuration options for a redoctober

View File

@@ -18,12 +18,12 @@ import (
"github.com/cloudflare/redoctober/keycache"
"github.com/cloudflare/redoctober/order"
"github.com/cloudflare/redoctober/passvault"
"github.com/cloudflare/redoctober/persist"
)
var (
crypt cryptor.Cryptor
crypt *cryptor.Cryptor
records passvault.Records
cache keycache.Cache
orders order.Orderer
)
@@ -177,14 +177,6 @@ type StatusData struct {
Status string
}
// Delegation restoration and persistance configuration follows.
const (
PDStateNeverPersist = "disabled"
PDStateNotPersisting = "inactive"
PDStateNowPersisting = "active"
)
var restore struct {
Config *config.Delegations
State string
@@ -199,7 +191,7 @@ func jsonStatusError(err error) ([]byte, error) {
return json.Marshal(ResponseData{Status: err.Error()})
}
func jsonSummary() ([]byte, error) {
return json.Marshal(SummaryData{Status: "ok", Live: cache.GetSummary(), All: records.GetSummary()})
return json.Marshal(SummaryData{Status: "ok", Live: crypt.LiveSummary(), All: records.GetSummary()})
}
func jsonResponse(resp []byte) ([]byte, error) {
return json.Marshal(ResponseData{Status: "ok", Response: resp})
@@ -273,11 +265,10 @@ func Init(path string, config *config.Config) error {
}
restore.Config = config.Delegations
restore.State = PDStateNeverPersist
restore.State = persist.Disabled
orders = order.NewOrderer(hipchatClient)
cache = keycache.Cache{UserKeys: make(map[keycache.DelegateIndex]keycache.ActiveUser)}
crypt = cryptor.New(&records, &cache)
crypt, err = cryptor.New(&records, nil, config)
return err
}
@@ -320,7 +311,6 @@ func Create(jsonIn []byte) ([]byte, error) {
func Summary(jsonIn []byte) ([]byte, error) {
var s SummaryRequest
var err error
cache.Refresh()
defer func() {
if err != nil {
@@ -330,6 +320,11 @@ func Summary(jsonIn []byte) ([]byte, error) {
}
}()
err = crypt.Refresh()
if err != nil {
return jsonStatusError(err)
}
if err := json.Unmarshal(jsonIn, &s); err != nil {
return jsonStatusError(err)
}
@@ -373,7 +368,11 @@ func Purge(jsonIn []byte) ([]byte, error) {
return jsonStatusError(err)
}
cache.FlushCache()
err = crypt.Flush()
if err != nil {
return jsonStatusError(err)
}
return jsonStatusOk()
}
@@ -426,7 +425,7 @@ func Delegate(jsonIn []byte) ([]byte, error) {
}
// add signed-in record to active set
if err = cache.AddKeyFromRecord(pr, s.Name, s.Password, s.Users, s.Labels, s.Uses, s.Slot, s.Time); err != nil {
if err = crypt.Delegate(pr, s.Name, s.Password, s.Users, s.Labels, s.Uses, s.Slot, s.Time); err != nil {
return jsonStatusError(err)
}
@@ -798,27 +797,32 @@ func Order(jsonIn []byte) (out []byte, err error) {
// Get the owners of the ciphertext.
owners, _, err := crypt.GetOwners(o.EncryptedData)
if err != nil {
jsonStatusError(err)
return jsonStatusError(err)
}
if o.Duration == "" {
err = errors.New("Duration required when placing an order.")
jsonStatusError(err)
return jsonStatusError(err)
}
if o.Uses == 0 {
err = errors.New("Number of required uses necessary when placing an order.")
jsonStatusError(err)
return jsonStatusError(err)
}
cache.Refresh()
err = crypt.Refresh()
if err != nil {
return jsonStatusError(err)
}
orderNum := order.GenerateNum()
if len(o.Users) == 0 {
err = errors.New("Must specify at least one user per order.")
jsonStatusError(err)
return jsonStatusError(err)
}
adminsDelegated, numDelegated := cache.DelegateStatus(o.Users[0], o.Labels, owners)
adminsDelegated, numDelegated := crypt.DelegateStatus(o.Users[0], o.Labels, owners)
duration, err := time.ParseDuration(o.Duration)
if err != nil {
jsonStatusError(err)
return jsonStatusError(err)
}
currentTime := time.Now()
ord := order.CreateOrder(o.Name,

View File

@@ -14,6 +14,7 @@ import (
"github.com/cloudflare/redoctober/config"
"github.com/cloudflare/redoctober/passvault"
"github.com/cloudflare/redoctober/persist"
)
func TestCreate(t *testing.T) {
@@ -97,9 +98,9 @@ func TestSummary(t *testing.T) {
if err != nil {
t.Fatalf("Error getting status, %v", err)
}
if st.Status != PDStateNeverPersist {
if st.Status != persist.Disabled {
t.Fatalf("Persistent delegations should be '%s' but are '%s'",
PDStateNeverPersist, st.Status)
persist.Disabled, st.Status)
}
respJson, err = Summary(createJson)
@@ -174,7 +175,7 @@ func TestSummary(t *testing.T) {
dataLive, ok := s.Live["Bob"]
if !ok {
t.Fatalf("Error in summary of account, record missing, %v", cache.UserKeys)
t.Fatalf("Error in summary of account, record missing, %v", crypt.LiveSummary())
}
if dataLive.Admin != false {
t.Fatalf("Error in summary of account, record missing")
@@ -184,7 +185,7 @@ func TestSummary(t *testing.T) {
}
var s1 SummaryData
delegations := cache.GetSummary()
delegations := crypt.LiveSummary()
if len(delegations) == 0 {
t.Fatal("no delegations active")
}
@@ -230,7 +231,7 @@ func TestSummary(t *testing.T) {
t.Fatal("Bob was removed from the list of users")
}
delegations = cache.GetSummary()
delegations = crypt.LiveSummary()
if len(delegations) != 0 {
t.Fatalf("purge failed to clear delegations (%d delegations remain)", len(delegations))
}
@@ -470,7 +471,11 @@ func TestEncryptDecrypt(t *testing.T) {
}
// check summary to see if none are delegated
cache.Refresh()
err = crypt.Refresh()
if err != nil {
t.Fatalf("Error in summary: %s", err)
}
respJson, err = Summary(summaryJson)
if err != nil {
t.Fatalf("Error in summary, %v", err)
@@ -557,7 +562,11 @@ func TestEncryptDecrypt(t *testing.T) {
}
// verify the presence of the two delgations
cache.Refresh()
err = crypt.Refresh()
if err != nil {
t.Fatalf("Error in summary: %s", err)
}
var sum2 SummaryData
respJson, err = Summary(summaryJson)
if err != nil {
@@ -936,7 +945,11 @@ func TestModify(t *testing.T) {
}
// check summary to see if none are delegated
cache.Refresh()
err = crypt.Refresh()
if err != nil {
t.Fatalf("Error refreshing: %s", err)
}
respJson, err = Summary(summaryJson)
if err != nil {
t.Fatalf("Error in summary, %v", err)
@@ -1078,6 +1091,7 @@ func TestStatic(t *testing.T) {
if err != nil {
t.Fatalf("Error opening file, %v", err)
}
defer os.Remove("/tmp/db1.json")
_, err = file.Write(diskVault)
if err != nil {
@@ -1139,9 +1153,10 @@ func TestStatic(t *testing.T) {
t.Fatalf("Error in summary, %v, %v", expected, r.Response)
}
cache.FlushCache()
os.Remove("/tmp/db1.json")
err = crypt.Flush()
if err != nil {
t.Fatalf("Error flushing cache: %s", err)
}
}
func TestValidateName(t *testing.T) {

View File

@@ -15,10 +15,12 @@ import (
"sort"
"strconv"
"github.com/cloudflare/redoctober/config"
"github.com/cloudflare/redoctober/keycache"
"github.com/cloudflare/redoctober/msp"
"github.com/cloudflare/redoctober/padding"
"github.com/cloudflare/redoctober/passvault"
"github.com/cloudflare/redoctober/persist"
"github.com/cloudflare/redoctober/symcrypt"
)
@@ -29,10 +31,25 @@ const (
type Cryptor struct {
records *passvault.Records
cache *keycache.Cache
persist persist.Store
}
func New(records *passvault.Records, cache *keycache.Cache) Cryptor {
return Cryptor{records, cache}
func New(records *passvault.Records, cache *keycache.Cache, config *config.Config) (*Cryptor, error) {
if cache == nil {
cache = &keycache.Cache{UserKeys: make(map[keycache.DelegateIndex]keycache.ActiveUser)}
}
store, err := persist.New(config.Delegations)
if err != nil {
return nil, err
}
c := &Cryptor{
records: records,
cache: cache,
persist: store,
}
return c, nil
}
// AccessStructure represents different possible access structures for
@@ -525,6 +542,10 @@ func (c *Cryptor) Encrypt(in []byte, labels []string, access AccessStructure) (r
// Decrypt decrypts a file using the keys in the key cache.
func (c *Cryptor) Decrypt(in []byte, user string) (resp []byte, labels, names []string, secure bool, err error) {
return c.decrypt(c.cache, in, user)
}
func (c *Cryptor) decrypt(cache *keycache.Cache, in []byte, user string) (resp []byte, labels, names []string, secure bool, err error) {
// unwrap encrypted file
var encrypted EncryptedData
if err = json.Unmarshal(in, &encrypted); err != nil {
@@ -563,7 +584,7 @@ func (c *Cryptor) Decrypt(in []byte, user string) (resp []byte, labels, names []
// decrypt file key with delegate keys
var unwrappedKey = make([]byte, 16)
unwrappedKey, names, err = encrypted.unwrapKey(c.cache, user)
unwrappedKey, names, err = encrypted.unwrapKey(cache, user)
if err != nil {
return
}
@@ -642,3 +663,110 @@ func (c *Cryptor) GetOwners(in []byte) (names []string, predicate string, err er
return
}
// LiveSummary returns a list of the users currently delegated.
func (c *Cryptor) LiveSummary() map[string]keycache.ActiveUser {
return c.cache.GetSummary()
}
// Refresh purges all expired or fully-used delegations in the
// crypto's key cache. It returns an error if the delegations
// should have been stored, but couldn't be.
func (c *Cryptor) Refresh() error {
n := c.cache.Refresh()
if n != 0 {
return c.store()
}
return nil
}
// Flush removes all delegations.
func (c *Cryptor) Flush() error {
if c.cache.Flush() {
return c.store()
}
return nil
}
// Delegate attempts to decrypt a key for the specified user and add
// the key to the key cache.
func (c *Cryptor) Delegate(record passvault.PasswordRecord, name, password string, users, labels []string, uses int, slot, durationString string) (err error) {
err = c.cache.AddKeyFromRecord(record, name, password, users, labels, uses, slot, durationString)
if err != nil {
return err
}
return c.store()
}
// DelegateStatus will return a list of admins who have delegated to a particular user, for a particular label.
// This is useful information to have when determining the status of an order and conveying order progress.
func (c *Cryptor) DelegateStatus(name string, labels, admins []string) (adminsDelegated []string, hasDelegated int) {
return c.cache.DelegateStatus(name, labels, admins)
}
var persistLabels = []string{"restore"}
// store serialises the key cache, encrypts it, and writes it to disk.
func (c *Cryptor) store() error {
// If the store isn't currently active, we shouldn't attempt
// to persist the store.
st := c.persist.Status()
if st.State != persist.Active {
return nil
}
cache, err := json.Marshal(c.cache.GetSummary())
if err != nil {
return err
}
access := AccessStructure{
Names: c.persist.Users(),
Predicate: c.persist.Policy(),
}
cache, err = c.Encrypt(cache, persistLabels, access)
if err != nil {
return err
}
return c.persist.Store(cache)
}
// ErrRestoreDelegations is a sentinal value returned when more
// delegations are needed for the restore to continue.
var ErrRestoreDelegations = errors.New("cryptor: need more delegations")
// Restore delegates the named user to the persistence key cache. If
// enough delegations are present to restore the cache, the current
// Red October key cache is replaced with the persisted one.
func (c *Cryptor) Restore(name, password string, uses int, slot, durationString string) error {
record, ok := c.records.GetRecord(name)
if !ok {
return errors.New("Missing user on disk")
}
err := c.persist.Delegate(record, name, password, c.persist.Users(), persistLabels, uses, slot, durationString)
if err != nil {
return err
}
// A failure to decrypt isn't an error, it just means there
// aren't enough delegations yet; the sentinal value
// ErrRestoreDelegations is returned to indicate this.
cache, _, _, _, err := c.decrypt(c.persist.Cache(), c.persist.Blob(), name)
if err != nil {
return ErrRestoreDelegations
}
var uk map[string]keycache.ActiveUser
err = json.Unmarshal(cache, &uk)
if err != nil {
return err
}
c.cache = keycache.NewFrom(uk)
c.persist.Persist()
return nil
}

View File

@@ -8,10 +8,14 @@ import (
"bytes"
"encoding/base64"
"encoding/json"
"io/ioutil"
"os"
"testing"
"github.com/cloudflare/redoctober/config"
"github.com/cloudflare/redoctober/keycache"
"github.com/cloudflare/redoctober/passvault"
"github.com/cloudflare/redoctober/persist"
)
func TestHash(t *testing.T) {
@@ -83,7 +87,14 @@ func TestDuplicates(t *testing.T) {
if err != nil {
t.Fatalf("%v", err)
}
c := Cryptor{&records, &cache}
cfg := &config.Delegations{Persist: false}
store, err := persist.New(cfg)
if err != nil {
t.Fatal(err.Error())
}
c := Cryptor{&records, &cache, store}
for _, name := range names {
pr, err := records.AddNewRecord(name, "weakpassword", true, passvault.DefaultRecordType)
@@ -117,6 +128,207 @@ func TestDuplicates(t *testing.T) {
t.Fatalf("That shouldn't have worked!")
}
cache.FlushCache()
cache.Flush()
}
}
func TestEncryptDecrypt(t *testing.T) {
// Setup total names and partitions.
names := []string{"Alice", "Bob", "Carl"}
recs := make(map[string]passvault.PasswordRecord, 0)
left := []string{"Alice", "Bob"}
right := []string{"Bob", "Carl"}
// Add each user to the keycache.
cache := keycache.NewCache()
records, err := passvault.InitFrom("memory")
if err != nil {
t.Fatalf("%v", err)
}
cfg := &config.Delegations{Persist: false}
store, err := persist.New(cfg)
if err != nil {
t.Fatal(err.Error())
}
c := Cryptor{&records, &cache, store}
for _, name := range names {
pr, err := records.AddNewRecord(name, "weakpassword", true, passvault.DefaultRecordType)
if err != nil {
t.Fatalf("%v", err)
}
recs[name] = pr
}
// Create candidate encryption of message.
ac := AccessStructure{
LeftNames: left,
RightNames: right,
}
resp, err := c.Encrypt([]byte("Hello World!"), []string{}, ac)
if err != nil {
t.Fatalf("Error: %s", err)
}
// Delegate all the things.
for name, pr := range recs {
err = cache.AddKeyFromRecord(pr, name, "weakpassword", nil, nil, 2, "", "1h")
if err != nil {
t.Fatalf("%v", err)
}
}
// (resp []byte, labels, names []string, secure bool, err error)
_, _, _, _, err = c.Decrypt(resp, "alice")
if err != nil {
t.Fatalf("%v", err)
}
}
func tempName() (string, error) {
tmpf, err := ioutil.TempFile("", "transport_cachedkp_")
if err != nil {
return "", err
}
name := tmpf.Name()
tmpf.Close()
return name, nil
}
func TestRestore(t *testing.T) {
const testUses = 5 // How many uses to delegate for.
// Get the temporary persisted file.
temp, err := tempName()
if err != nil {
t.Fatal(err)
}
defer os.Remove(temp)
// Setup total names and partitions.
names := []string{"Alice", "Bob", "Carl"}
recs := make(map[string]passvault.PasswordRecord, 0)
// Add each user to the keycache.
cache := keycache.NewCache()
records, err := passvault.InitFrom("memory")
if err != nil {
t.Fatalf("%v", err)
}
for _, name := range names {
pr, err := records.AddNewRecord(name, "weakpassword", true, passvault.DefaultRecordType)
if err != nil {
t.Fatalf("%v", err)
}
recs[name] = pr
}
alice, ok := records.GetRecord("Alice")
if !ok {
t.Fatal("Alice not found in password vault.")
}
carl, ok := records.GetRecord("Carl")
if !ok {
t.Fatal("Carl not found in password vault.")
}
// First, simulate a running Red October with persistence.
cfg := &config.Delegations{
Persist: true,
Mechanism: persist.FileMechanism,
Location: temp,
Policy: "(Alice & Bob) | (Bob & Carl)",
Users: []string{"Alice", "Bob", "Carl"},
}
store, err := persist.New(cfg)
if err != nil {
t.Fatal(err.Error())
}
c := Cryptor{&records, &cache, store}
c.persist.Persist()
err = c.Delegate(alice, "Alice", "weakpassword", []string{"Bob"}, []string{},
testUses, "", "1h")
if err != nil {
t.Fatal(err)
}
err = c.Delegate(carl, "Carl", "weakpassword", []string{"Bob"}, []string{},
testUses, "", "1h")
// Next, simulate restarting that server.
store, err = persist.New(cfg)
if err != nil {
t.Fatal(err.Error())
}
c = Cryptor{&records, &cache, store}
if _, err := os.Stat(temp); err != nil {
t.Fatalf("Not persisting: %v", err)
}
err = c.Restore("Alice", "weakpassword", 2, "", "1h")
if err != ErrRestoreDelegations {
t.Fatal(err)
}
err = c.Restore("Carl", "weakpassword", 2, "", "1h")
if err != ErrRestoreDelegations {
t.Fatal(err)
}
status := c.persist.Status()
if status.State != persist.Inactive {
t.Fatalf("The persistent delegations should be %s, not %s",
persist.Inactive, status.State)
}
err = c.Restore("Bob", "weakpassword", 2, "", "1h")
if err != nil {
t.Fatal(err)
}
status = c.persist.Status()
if status.State != persist.Active {
t.Fatalf("The persistent delegations should be %s, not %s",
persist.Active, status.State)
}
if len(c.cache.UserKeys) != 2 {
t.Fatalf("Delegations do not seem to have been restored.")
}
usage, ok := c.cache.UserKeys[keycache.DelegateIndex{Name: "Alice"}]
if !ok {
t.Fatalf("Alice not found in active delegations.")
}
if usage.Uses != testUses {
t.Fatalf("Invalid number of uses in restored delegations.")
}
usage, ok = c.cache.UserKeys[keycache.DelegateIndex{Name: "Carl"}]
if !ok {
t.Fatalf("Carl not found in active delegations.")
}
if usage.Uses != testUses {
t.Fatalf("Invalid number of uses in restored delegations.")
}
_, ok = c.cache.UserKeys[keycache.DelegateIndex{Name: "Bob"}]
if ok {
t.Fatalf("Bob shouldn't be in the active delegations.")
}
}

View File

@@ -14,6 +14,7 @@ import (
"errors"
"fmt"
"log"
"strings"
"time"
"github.com/cloudflare/redoctober/ecdh"
@@ -94,6 +95,24 @@ func NewCache() Cache {
return Cache{make(map[DelegateIndex]ActiveUser)}
}
// NewFrom takes the output of GetSummary and returns a new keycache.
func NewFrom(summary map[string]ActiveUser) *Cache {
cache := &Cache{make(map[DelegateIndex]ActiveUser)}
for di, user := range summary {
diSplit := strings.SplitN(di, "-", 2)
index := DelegateIndex{
Name: diSplit[0],
}
if len(diSplit) == 2 {
index.Slot = diSplit[1]
}
cache.UserKeys[index] = user
}
return cache
}
// setUser takes an ActiveUser and adds it to the cache.
func (cache *Cache) setUser(in ActiveUser, name, slot string) {
cache.UserKeys[DelegateIndex{Name: name, Slot: slot}] = in
@@ -155,21 +174,35 @@ func (cache *Cache) GetSummary() map[string]ActiveUser {
return summaryData
}
// FlushCache removes all delegated keys.
func (cache *Cache) FlushCache() {
// FlushCache removes all delegated keys. It returns true if the cache
// wasn't empty (i.e. there were active users removed), and false if
// the cache was empty.
func (cache *Cache) Flush() bool {
if len(cache.UserKeys) == 0 {
return false
}
for d := range cache.UserKeys {
delete(cache.UserKeys, d)
}
return true
}
// Refresh purges all expired or used up keys.
func (cache *Cache) Refresh() {
// Refresh purges all expired keys. It returns the number of
// delegations that were removed.
func (cache *Cache) Refresh() int {
var removed int
for d, active := range cache.UserKeys {
if active.Usage.Expiry.Before(time.Now()) {
log.Println("Record expired", d.Name, d.Slot, active.Usage.Users, active.Usage.Labels, active.Usage.Expiry)
removed++
delete(cache.UserKeys, d)
}
}
return removed
}
// AddKeyFromRecord decrypts a key for a given record and adds it to the cache.

View File

@@ -32,7 +32,11 @@ func TestUsesFlush(t *testing.T) {
t.Fatalf("%v", err)
}
cache.Refresh()
removed := cache.Refresh()
if removed != 0 {
t.Fatalf("No active users should have been removed")
}
if len(cache.UserKeys) != 1 {
t.Fatalf("Error in number of live keys")
}
@@ -150,7 +154,6 @@ func TestGoodLabel(t *testing.T) {
t.Fatalf("%v", err)
}
cache.Refresh()
if len(cache.UserKeys) != 0 {
t.Fatalf("Error in number of live keys %v", cache.UserKeys)
}
@@ -291,3 +294,139 @@ func TestBadUser(t *testing.T) {
t.Fatalf("Error in number of live keys %v", cache.UserKeys)
}
}
func TestRefresh(t *testing.T) {
records, err := passvault.InitFrom("memory")
if err != nil {
t.Fatalf("%v", err)
}
pr, err := records.AddNewRecord("user", "weakpassword", true, passvault.DefaultRecordType)
if err != nil {
t.Fatalf("%v", err)
}
cache := NewCache()
err = cache.AddKeyFromRecord(
pr, "user", "weakpassword",
[]string{"ci", "buildeng", "user"},
[]string{"red", "blue"},
1, "", "1s",
)
if err != nil {
t.Fatalf("%v", err)
}
removed := cache.Refresh()
if removed != 0 {
t.Fatalf("Refresh should not have removed any active users.")
}
time.Sleep(2 * time.Second)
removed = cache.Refresh()
if removed != 1 {
t.Fatalf("Refresh should have removed an active user, removed %d", removed)
}
if len(cache.GetSummary()) != 0 {
t.Fatalf("There should be no active users in the cache, but there are %d", len(cache.GetSummary()))
}
}
func cmpEntry(c, d ActiveUser) bool {
if c.Uses != d.Uses {
return false
}
if len(c.Labels) != len(d.Labels) {
return false
}
for i := range c.Labels {
if c.Labels[i] != d.Labels[i] {
return false
}
}
if len(c.Users) != len(d.Users) {
return false
}
for i := range c.Users {
if c.Users[i] != d.Users[i] {
return false
}
}
if c.Expiry != d.Expiry {
return false
}
return true
}
func cmpCache(a, b Cache) bool {
if len(a.UserKeys) != len(b.UserKeys) {
return false
}
for aIndex, aUser := range a.UserKeys {
bUser, ok := b.UserKeys[aIndex]
if !ok {
return false
}
if !cmpEntry(aUser, bUser) {
return false
}
}
return true
}
func TestNewFrom(t *testing.T) {
records, err := passvault.InitFrom("memory")
if err != nil {
t.Fatalf("%v", err)
}
cache := NewCache()
users := []string{"alice", "bob", "carol"}
for _, user := range users {
pr, err := records.AddNewRecord(user, "weakpassword", true, passvault.DefaultRecordType)
if err != nil {
t.Fatalf("%v", err)
}
err = cache.AddKeyFromRecord(
pr, user, "weakpassword",
[]string{"ci", "buildeng", "user"},
[]string{"red", "blue"},
1, "", "1h")
if err != nil {
t.Fatalf("%v", err)
}
}
pr, ok := records.GetRecord("alice")
if !ok {
t.Fatal("Couldn't retrieve 'alice' record.")
}
err = cache.AddKeyFromRecord(pr, "alice", "weakpassword",
[]string{"ci", "alice"}, []string{"blue", "yellow"},
2, "slotname", "1h")
if err != nil {
t.Fatal(err)
}
summary := cache.GetSummary()
cache2 := NewFrom(summary)
if !cmpCache(cache, *cache2) {
t.Fatal("caches don't match")
}
}

122
persist/file.go Normal file
View File

@@ -0,0 +1,122 @@
package persist
import (
"io/ioutil"
"os"
"github.com/cloudflare/redoctober/config"
"github.com/cloudflare/redoctober/keycache"
"github.com/cloudflare/redoctober/passvault"
)
// File implements a file-backed persistence store.
type File struct {
config *config.Delegations
cache *keycache.Cache
state string
blob []byte
}
// Valid ensures the configuration is valid for a file store. Note
// that it won't validate the policy, it will just ensure that one
// is present.
func (f *File) Valid() bool {
if f.config.Persist == false {
return false
}
if f.config.Policy == "" {
return false
}
if len(f.config.Users) == 0 {
return false
}
if f.config.Mechanism != FileMechanism {
return false
}
if f.config.Location == "" {
return false
}
return true
}
// newFile returns a new file-backed persistence store.
func newFile(config *config.Delegations) (Store, error) {
cache := keycache.NewCache()
file := &File{
config: config,
cache: &cache,
state: Inactive,
}
if !file.Valid() {
return nil, ErrInvalidConfig
}
err := file.Load()
if err != nil {
return nil, err
}
return file, nil
}
func (f *File) Blob() []byte {
return f.blob
}
func (f *File) Policy() string {
return f.config.Policy
}
func (f *File) Users() []string {
return f.config.Users
}
func (f *File) Store(blob []byte) error {
if f.state == Active {
f.blob = blob
return ioutil.WriteFile(f.config.Location, blob, 0644)
}
return nil
}
func (f *File) Load() error {
in, err := ioutil.ReadFile(f.config.Location)
if err != nil {
// If the file doesn't exist, it can be persisted
// immediately.
if os.IsNotExist(err) {
f.state = Active
return nil
}
return err
}
f.state = Inactive
f.blob = in
return nil
}
func (f *File) Persist() {
f.state = Active
}
func (f *File) Cache() *keycache.Cache {
return f.cache
}
func (f *File) Delegate(record passvault.PasswordRecord, name, password string, users, labels []string, uses int, slot, durationString string) error {
return f.cache.AddKeyFromRecord(record, name, password, users, labels, uses, slot, durationString)
}
func (f *File) Status() *Status {
return &Status{
State: f.state,
Summary: f.cache.GetSummary(),
}
}

137
persist/file_test.go Normal file
View File

@@ -0,0 +1,137 @@
package persist
import (
"io/ioutil"
"os"
"testing"
"github.com/cloudflare/redoctober/config"
)
func TestFileConfig(t *testing.T) {
cfg := &config.Delegations{
Persist: false,
}
f := &File{config: cfg}
if f.Valid() {
t.Fatal("persist: File config should persist")
}
cfg.Persist = true
if f.Valid() {
t.Fatal("persist: File config should require policy")
}
cfg.Policy = "some policy"
if f.Valid() {
t.Fatal("persist: File config should require mechanism")
}
cfg.Users = []string{"alice", "bob"}
cfg.Mechanism = "db"
if f.Valid() {
t.Fatalf("persist: File config should require the '%s' mechanism", FileMechanism)
}
cfg.Mechanism = FileMechanism
if f.Valid() {
t.Fatal("persist: File config should require a location")
}
cfg.Location = "testdata/store.bin"
if !f.Valid() {
t.Fatal("persist: valid File config marked as invalid")
}
cfg.Location = ""
_, err := New(cfg)
if err != ErrInvalidConfig {
t.Fatalf("persist: expected err='%s', have err='%s'",
ErrInvalidConfig, err)
}
}
func tempName() (string, error) {
tmpf, err := ioutil.TempFile("", "transport_cachedkp_")
if err != nil {
return "", err
}
name := tmpf.Name()
tmpf.Close()
return name, nil
}
func TestFileSanity(t *testing.T) {
sf, err := tempName()
if err != nil {
t.Fatal(err)
}
defer os.Remove(sf)
const expected = "testdata"
err = ioutil.WriteFile(sf, []byte(expected), 0644)
if err != nil {
t.Fatal(err)
}
cfg := &config.Delegations{
Persist: true,
Mechanism: FileMechanism,
Policy: "alice & bob",
Users: []string{"alice", "bob"},
Location: sf,
}
f, err := New(cfg)
if err != nil {
t.Fatal(err)
}
if string(f.Blob()) != expected {
t.Fatalf("persist: expected blob data '%s' but have '%s'", expected, f.Blob())
}
if f.Policy() != cfg.Policy {
t.Fatalf("persist: policy mismatch - should have '%s' but have '%s'",
cfg.Policy, f.Policy())
}
if len(f.Users()) != 2 {
t.Fatalf("persist: expected 2 users, have %d", len(f.Users()))
}
const expected2 = "test data"
if err = f.Store([]byte(expected2)); err != nil {
t.Fatal(err)
}
if string(f.Blob()) == expected2 {
t.Fatal("persist: should not have begun persisting yet")
}
f.Persist()
if err = f.Store([]byte(expected2)); err != nil {
t.Fatal(err)
}
if string(f.Blob()) != expected2 {
t.Fatalf("persist: expected blob data '%s' but have '%s'", expected2, f.Blob())
}
err = ioutil.WriteFile(sf, []byte(expected), 0644)
if err != nil {
t.Fatal(err)
}
if err = f.Load(); err != nil {
t.Fatal(err)
}
if string(f.Blob()) != expected {
t.Fatalf("persist: expected blob data '%s' but have '%s'", expected2, f.Blob())
}
}

59
persist/null.go Normal file
View File

@@ -0,0 +1,59 @@
package persist
import (
"errors"
"github.com/cloudflare/redoctober/config"
"github.com/cloudflare/redoctober/keycache"
"github.com/cloudflare/redoctober/passvault"
)
// Null is a non-persisting store. It is used when persistence is not
// activated.
type Null struct {
config *config.Delegations
}
func newNull(config *config.Delegations) (Store, error) {
return &Null{config: config}, nil
}
func (n *Null) Blob() []byte {
return nil
}
func (n *Null) Policy() string {
return n.config.Policy
}
func (n *Null) Users() []string {
return n.config.Users
}
func (n *Null) Store(bs []byte) error {
return nil
}
func (n *Null) Load() error {
return nil
}
func (n *Null) Persist() {
return
}
func (n *Null) Status() *Status {
return &Status{
State: Disabled,
Summary: nil,
}
}
func (n *Null) Delegate(record passvault.PasswordRecord, name, password string, users, labels []string, uses int, slot, durationString string) error {
return errors.New("persist: null store does not support delegations")
}
func (n *Null) Cache() *keycache.Cache {
cache := keycache.NewCache()
return &cache
}

60
persist/null_test.go Normal file
View File

@@ -0,0 +1,60 @@
package persist
import (
"testing"
"github.com/cloudflare/redoctober/config"
"github.com/cloudflare/redoctober/passvault"
)
func TestNewNull(t *testing.T) {
cfg := &config.Delegations{
Persist: false,
Mechanism: FileMechanism,
Location: "testdata/store.bin",
Policy: "policy",
}
store, err := New(cfg)
if err != nil {
t.Fatalf("persist: failed to create a new store: %s", err)
}
if _, ok := store.(*Null); !ok {
t.Fatalf("persist: expected a Null store, but have %T", store)
}
if store.Blob() != nil {
t.Fatalf("persist: Null store should return an empty blob")
}
if store.Policy() != cfg.Policy {
t.Fatalf("persist: expected a consistent policy")
}
if err := store.Store([]byte("test data")); err != nil {
t.Fatalf("persist: Null.Store failed with %s", err)
}
if err := store.Load(); err != nil {
t.Fatalf("persist: Null.Load failed with %s", err)
}
status := store.Status()
if status.State != Disabled {
t.Fatalf("persist: Null store should never persist")
}
if len(status.Summary) != 0 {
t.Fatal("persist: Null summary should have zero entries")
}
err = store.Delegate(passvault.PasswordRecord{}, "name", "password", []string{}, []string{}, 1, "", "1h")
if err == nil {
t.Fatal("persist: expected delegation to fail")
}
if cache := store.Cache(); len(cache.UserKeys) != 0 {
t.Fatal("persist: Null Cache should return an empty cache")
}
}

81
persist/persist.go Normal file
View File

@@ -0,0 +1,81 @@
// Package persist implements delegation persistence. It is primarily
// concerned with configuration and serialisation; encryption and
// decryption is done by the cryptor package.
package persist
import (
"errors"
"github.com/cloudflare/redoctober/config"
"github.com/cloudflare/redoctober/keycache"
"github.com/cloudflare/redoctober/passvault"
)
var defaultStore Store = &File{}
const (
// NeverPersist indicates that the persistence store will
// never persist active delegations.
Disabled = "disabled"
// Inactive indicates that the persistence store requires
// more delegations to unlock, and isn't currently persisting
// the store.
Inactive = "inactive"
// Active indicates that the persistence store is
// actively persisting delegations.
Active = "active"
)
// Status contains information on the current status of a persistence
// store.
type Status struct {
State string `json:"state"`
Summary map[string]keycache.ActiveUser
}
// Store is a persistence store interface that handles delegations,
// serialising the persistence store, and writing the store to disk.
type Store interface {
Blob() []byte
Policy() string
Users() []string
Store([]byte) error
Load() error
Status() *Status
// Persist tells the Store to start actively persisting.
Persist()
Delegate(record passvault.PasswordRecord, name, password string, users, labels []string, uses int, slot, durationString string) error
// This is not the main keycache. This is the keycache for
// users that can decrypt the store.
Cache() *keycache.Cache
}
const FileMechanism = "file"
type mechanism func(*config.Delegations) (Store, error)
var stores = map[string]mechanism{
"": newNull,
FileMechanism: newFile,
}
func New(config *config.Delegations) (Store, error) {
if config == nil {
return nil, errors.New("persist: nil configuration")
}
if !config.Persist {
return newNull(config)
}
constructor, ok := stores[config.Mechanism]
if !ok {
return nil, errors.New("persist: invalid persistence mechanism")
}
return constructor(config)
}
var ErrInvalidConfig = errors.New("persist: invalid configuration")

26
persist/persist_test.go Normal file
View File

@@ -0,0 +1,26 @@
package persist
import (
"testing"
"github.com/cloudflare/redoctober/config"
)
func TestNew(t *testing.T) {
cfg := &config.Delegations{
Persist: true,
Policy: "policy",
Users: []string{"alice"},
Mechanism: FileMechanism,
Location: "testdata/store.bin",
}
store, err := New(cfg)
if err != nil {
t.Fatalf("persist: failed to create a new store: %s", err)
}
if _, ok := store.(*File); !ok {
t.Fatalf("persist: New should return a *File, but returned a %T", store)
}
}