diff --git a/config/config.go b/config/config.go index e38f7e5..4ae6ad7 100644 --- a/config/config.go +++ b/config/config.go @@ -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 diff --git a/core/core.go b/core/core.go index 7e71c4e..3125cdf 100644 --- a/core/core.go +++ b/core/core.go @@ -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, diff --git a/core/core_test.go b/core/core_test.go index 1af8b99..ed6fb21 100644 --- a/core/core_test.go +++ b/core/core_test.go @@ -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) { diff --git a/cryptor/cryptor.go b/cryptor/cryptor.go index f598069..fd7a126 100644 --- a/cryptor/cryptor.go +++ b/cryptor/cryptor.go @@ -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 +} diff --git a/cryptor/cryptor_test.go b/cryptor/cryptor_test.go index fa4727c..85ddc80 100644 --- a/cryptor/cryptor_test.go +++ b/cryptor/cryptor_test.go @@ -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.") + } + +} diff --git a/keycache/keycache.go b/keycache/keycache.go index 77d2218..9268964 100644 --- a/keycache/keycache.go +++ b/keycache/keycache.go @@ -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. diff --git a/keycache/keycache_test.go b/keycache/keycache_test.go index d4049aa..26a0f2e 100644 --- a/keycache/keycache_test.go +++ b/keycache/keycache_test.go @@ -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") + } + +} diff --git a/persist/file.go b/persist/file.go new file mode 100644 index 0000000..b2f1cf2 --- /dev/null +++ b/persist/file.go @@ -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(), + } +} diff --git a/persist/file_test.go b/persist/file_test.go new file mode 100644 index 0000000..1bea127 --- /dev/null +++ b/persist/file_test.go @@ -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()) + } + +} diff --git a/persist/null.go b/persist/null.go new file mode 100644 index 0000000..8d9d4fd --- /dev/null +++ b/persist/null.go @@ -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 +} diff --git a/persist/null_test.go b/persist/null_test.go new file mode 100644 index 0000000..af972cc --- /dev/null +++ b/persist/null_test.go @@ -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") + } +} diff --git a/persist/persist.go b/persist/persist.go new file mode 100644 index 0000000..07384ae --- /dev/null +++ b/persist/persist.go @@ -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") diff --git a/persist/persist_test.go b/persist/persist_test.go new file mode 100644 index 0000000..dff35c8 --- /dev/null +++ b/persist/persist_test.go @@ -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) + } +}