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-08-04 16:10:00 -07:00
parent 1cf72b1f6d
commit 7c95007cda
13 changed files with 1074 additions and 48 deletions
+131 -3
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
}
+214 -2
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.")
}
}