diff --git a/core/core.go b/core/core.go index 51608ac..5bf5ec9 100644 --- a/core/core.go +++ b/core/core.go @@ -9,9 +9,13 @@ import ( "errors" "fmt" "log" + "strconv" + "time" "github.com/cloudflare/redoctober/cryptor" + "github.com/cloudflare/redoctober/hipchat" "github.com/cloudflare/redoctober/keycache" + "github.com/cloudflare/redoctober/order" "github.com/cloudflare/redoctober/passvault" ) @@ -19,6 +23,7 @@ var ( crypt cryptor.Cryptor records passvault.Records cache keycache.Cache + orders order.Orderer ) // Each of these structures corresponds to the JSON expected on the @@ -63,6 +68,7 @@ type PasswordRequest struct { Password string NewPassword string + HipchatName string } type EncryptRequest struct { @@ -106,6 +112,33 @@ type ExportRequest struct { Password string } +type OrderRequest struct { + Name string + Password string + Duration string + Uses string + Data []byte + Label string +} + +type OrderInfoRequest struct { + Name string + Password string + + OrderNum string +} +type OrderOutstandingRequest struct { + Name string + Password string +} + +type OrderCancelRequest struct { + Name string + Password string + + OrderNum string +} + // These structures map the JSON responses that will be sent from the API type ResponseData struct { @@ -169,8 +202,7 @@ func validateUser(name, password string, admin bool) error { return nil } -// validateName checks that the username and password pass the minimal -// validation check +// validateName checks that the username and password pass a validation test. func validateName(name, password string) error { if name == "" { return errors.New("User name must not be blank") @@ -183,21 +215,35 @@ func validateName(name, password string) error { } // Init reads the records from disk from a given path -func Init(path string) error { +func Init(ca, hcKey, hcRoom, hcHost, roHost string) error { var err error defer func() { if err != nil { log.Printf("core.init failed: %v", err) } else { - log.Printf("core.init success: path=%s", path) + log.Printf("core.init success: ca=%s", ca) } }() - if records, err = passvault.InitFrom(path); err != nil { - err = fmt.Errorf("failed to load password vault %s: %s", path, err) + if records, err = passvault.InitFrom(ca); err != nil { + err = fmt.Errorf("failed to load password vault %s: %s", ca, err) } + var hipchatClient hipchat.HipchatClient + if hcKey != "" && hcRoom != "" && hcHost != "" { + roomId, err := strconv.Atoi(hcRoom) + if err != nil { + return errors.New("core.init unable to use hipchat roomId provided") + } + hipchatClient = hipchat.HipchatClient{ + ApiKey: hcKey, + RoomId: roomId, + HcHost: hcHost, + RoHost: roHost, + } + } + orders = order.NewOrderer(hipchatClient) cache = keycache.Cache{UserKeys: make(map[keycache.DelegateIndex]keycache.ActiveUser)} crypt = cryptor.New(&records, &cache) @@ -352,6 +398,22 @@ func Delegate(jsonIn []byte) ([]byte, error) { return jsonStatusError(err) } + // Make sure we capture the number who have already delegated. + for _, delegatedUser := range s.Users { + for _, delegatedLabel := range s.Labels { + if orderKey, found := orders.FindOrder(delegatedUser, delegatedLabel); found { + order := orders.Orders[orderKey] + order.AdminsDelegated = append(order.AdminsDelegated, s.Name) + order.Delegated++ + orders.Orders[orderKey] = order + + // Notify the hipchat room that there was a new delegator + orders.NotifyDelegation(s.Name, delegatedLabel, delegatedUser, orderKey, s.Time) + + } + } + } + return jsonStatusOk() } @@ -423,7 +485,7 @@ func Password(jsonIn []byte) ([]byte, error) { } // add signed-in record to active set - err = records.ChangePassword(s.Name, s.Password, s.NewPassword) + err = records.ChangePassword(s.Name, s.Password, s.NewPassword, s.HipchatName) if err != nil { return jsonStatusError(err) } @@ -490,7 +552,7 @@ func ReEncrypt(jsonIn []byte) ([]byte, error) { return jsonStatusError(err) } - data, _, secure, err := crypt.Decrypt(s.Data, s.Name) + data, _, _, secure, err := crypt.Decrypt(s.Data, s.Name) if err != nil { return jsonStatusError(err) } @@ -535,7 +597,7 @@ func Decrypt(jsonIn []byte) ([]byte, error) { return jsonStatusError(err) } - data, names, secure, err := crypt.Decrypt(s.Data, s.Name) + data, allLabels, names, secure, err := crypt.Decrypt(s.Data, s.Name) if err != nil { return jsonStatusError(err) } @@ -551,6 +613,13 @@ func Decrypt(jsonIn []byte) ([]byte, error) { return jsonStatusError(err) } + // Cleanup any orders that have been fulfilled and notify the room. + for _, label := range allLabels { + if orderKey, found := orders.FindOrder(s.Name, label); found { + delete(orders.Orders, orderKey) + orders.NotifyOrderFulfilled(s.Name, orderKey) + } + } return jsonResponse(out) } @@ -661,3 +730,158 @@ func Export(jsonIn []byte) ([]byte, error) { return jsonResponse(out) } + +// Order will request delegations from other users. +func Order(jsonIn []byte) (out []byte, err error) { + var o OrderRequest + + defer func() { + if err != nil { + log.Printf("core.order failed: user=%s %v", o.Name, err) + } else { + log.Printf("core.order success: user=%s", o.Name) + } + }() + + if err = json.Unmarshal(jsonIn, &o); err != nil { + return jsonStatusError(err) + } + + if err := validateUser(o.Name, o.Password, false); err != nil { + return jsonStatusError(err) + } + + // Get the owners of the ciphertext. + owners, _, err := crypt.GetOwners(o.Data) + if err != nil { + jsonStatusError(err) + } + if o.Duration == "" { + err = errors.New("Duration required when placing an order.") + jsonStatusError(err) + } + if o.Uses == "" || o.Uses == "0" { + err = errors.New("Number of required uses necessary when placing an order.") + jsonStatusError(err) + } + cache.Refresh() + orderNum := order.GenerateNum() + + adminsDelegated, numDelegated := cache.DelegateStatus(o.Name, o.Label, owners) + duration, err := time.ParseDuration(o.Duration) + if err != nil { + jsonStatusError(err) + } + currentTime := time.Now() + expiryTime := currentTime.Add(duration) + ord := order.CreateOrder(o.Name, + o.Label, + orderNum, + currentTime, + expiryTime, + duration, + adminsDelegated, + owners, + numDelegated) + orders.Orders[orderNum] = ord + out, err = json.Marshal(ord) + + // Get a map to any alternative name we want to notify + altOwners := records.GetAltNamesFromName(orders.AlternateName, owners) + + // Let everyone on hipchat know there is a new order. + orders.NotifyNewOrder(o.Name, o.Duration, o.Label, o.Uses, orderNum, altOwners) + if err != nil { + return jsonStatusError(err) + } + return jsonResponse(out) +} + +// OrdersOutstanding will return a list of currently outstanding orders. +func OrdersOutstanding(jsonIn []byte) (out []byte, err error) { + var o OrderOutstandingRequest + + defer func() { + if err != nil { + log.Printf("core.ordersout failed: user=%s %v", o.Name, err) + } else { + log.Printf("core.ordersout success: user=%s", o.Name) + } + }() + + if err = json.Unmarshal(jsonIn, &o); err != nil { + return jsonStatusError(err) + } + + if err := validateUser(o.Name, o.Password, false); err != nil { + return jsonStatusError(err) + } + + out, err = json.Marshal(orders.Orders) + if err != nil { + return jsonStatusError(err) + } + return jsonResponse(out) +} + +// OrderInfo will return a list of currently outstanding order numbers. +func OrderInfo(jsonIn []byte) (out []byte, err error) { + var o OrderInfoRequest + + defer func() { + if err != nil { + log.Printf("core.order failed: user=%s %v", o.Name, err) + } else { + log.Printf("core.order success: user=%s", o.Name) + } + }() + + if err = json.Unmarshal(jsonIn, &o); err != nil { + return jsonStatusError(err) + } + if err := validateUser(o.Name, o.Password, false); err != nil { + return jsonStatusError(err) + } + + if ord, ok := orders.Orders[o.OrderNum]; ok { + if out, err = json.Marshal(ord); err != nil { + return jsonStatusError(err) + } else if len(out) == 0 { + return jsonStatusError(errors.New("No order with that number")) + } + + return jsonResponse(out) + } + return +} + +// OrderCancel will cancel an order given an order num +func OrderCancel(jsonIn []byte) (out []byte, err error) { + var o OrderCancelRequest + + defer func() { + if err != nil { + log.Printf("core.order failed: user=%s %v", o.Name, err) + } else { + log.Printf("core.order success: user=%s", o.Name) + } + }() + + if err = json.Unmarshal(jsonIn, &o); err != nil { + return jsonStatusError(err) + } + + if err := validateUser(o.Name, o.Password, false); err != nil { + return jsonStatusError(err) + } + + if ord, ok := orders.Orders[o.OrderNum]; ok { + if o.Name == ord.Name { + delete(orders.Orders, o.OrderNum) + out = []byte("Successfully removed order") + return jsonResponse(out) + } + } + err = errors.New("Invalid Order Number") + return jsonStatusError(err) +} diff --git a/core/core_test.go b/core/core_test.go index 9951f74..6b00f05 100644 --- a/core/core_test.go +++ b/core/core_test.go @@ -18,7 +18,7 @@ import ( func TestCreate(t *testing.T) { createJson := []byte("{\"Name\":\"Alice\",\"Password\":\"Hello\"}") - Init("memory") + Init("memory", "", "", "", "") respJson, err := Create(createJson) if err != nil { @@ -66,7 +66,7 @@ func TestSummary(t *testing.T) { t.Fatalf("Error in summary of account with no vault, %v", s.Status) } - Init("memory") + Init("memory", "", "", "", "") // check for summary of initialized vault respJson, err = Create(createJson) @@ -220,7 +220,7 @@ func TestCreateUser(t *testing.T) { createUserECCJson := []byte("{\"Name\":\"Cat\",\"Password\":\"Cheshire\",\"UserType\":\"ECC\"}") createVaultJson := []byte("{\"Name\":\"Alice\",\"Password\":\"Hello\"}") - Init("memory") + Init("memory", "", "", "", "") // Check that users cannot be created before a vault is respJson, err := CreateUser(createUserJson) @@ -292,7 +292,7 @@ func TestPassword(t *testing.T) { delegateJson2 := []byte("{\"Name\":\"Alice\",\"Password\":\"Olleh\",\"Time\":\"2h\",\"Uses\":1}") passwordJson2 := []byte("{\"Name\":\"Alice\",\"Password\":\"Olleh\",\"NewPassword\":\"Hello\"}") - Init("memory") + Init("memory", "", "", "", "") // check for summary of initialized vault with new member var s ResponseData @@ -405,7 +405,7 @@ func TestEncryptDecrypt(t *testing.T) { encryptJson2 := []byte("{\"Name\":\"Alice\",\"Password\":\"Hello\",\"Minimum\":2,\"Owners\":[\"Alice\",\"Bob\",\"Carol\"],\"Data\":\"SGVsbG8gSmVsbG8=\",\"Labels\":[\"blue\",\"red\"]}") encryptJson3 := []byte("{\"Name\":\"Alice\",\"Password\":\"Hello\",\"Minimum\":1,\"Owners\":[\"Alice\"],\"Data\":\"SGVsbG8gSmVsbG8=\"}") - Init("memory") + Init("memory", "", "", "", "") // check for summary of initialized vault with new member var s ResponseData @@ -632,7 +632,7 @@ func TestReEncrypt(t *testing.T) { delegateJson7 := []byte(`{"Name":"Carol","Password":"Hello","Time":"10s","Uses":2,"Users":["Alice"],"Labels":["red"]}`) encryptJson := []byte(`{"Name":"Carol","Password":"Hello","Minimum":2,"Owners":["Alice","Bob","Carol"],"Data":"SGVsbG8gSmVsbG8=","Labels":["blue"]}`) - Init("memory") + Init("memory", "", "", "", "") // check for summary of initialized vault with new member var s ResponseData @@ -812,7 +812,7 @@ func TestOwners(t *testing.T) { var s ResponseData var l OwnersData - Init("memory") + Init("memory", "", "", "", "") Create(delegateJson) Delegate(delegateJson2) @@ -868,7 +868,7 @@ func TestModify(t *testing.T) { modifyJson4 := []byte("{\"Name\":\"Carol\",\"Password\":\"Hello\",\"ToModify\":\"Alice\",\"Command\":\"revoke\"}") modifyJson5 := []byte("{\"Name\":\"Carol\",\"Password\":\"Hello\",\"ToModify\":\"Alice\",\"Command\":\"delete\"}") - Init("memory") + Init("memory", "", "", "", "") // check for summary of initialized vault with new member var s ResponseData @@ -1059,7 +1059,7 @@ func TestStatic(t *testing.T) { t.Fatalf("Error closing file, %v", err) } - Init("/tmp/db1.json") + Init("/tmp/db1.json", "", "", "", "") // check for summary of initialized vault with new member var s ResponseData diff --git a/cryptor/cryptor.go b/cryptor/cryptor.go index 2944fa6..c62a0cf 100644 --- a/cryptor/cryptor.go +++ b/cryptor/cryptor.go @@ -524,14 +524,14 @@ 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, names []string, secure bool, err error) { +func (c *Cryptor) Decrypt(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 { return } if encrypted.Version != DEFAULT_VERSION && encrypted.Version != -1 { - return nil, nil, secure, errors.New("Unknown version") + return nil, nil, nil, secure, errors.New("Unknown version") } secure = encrypted.Version == -1 @@ -551,7 +551,7 @@ func (c *Cryptor) Decrypt(in []byte, user string) (resp []byte, names []string, return } if encrypted.VaultId != vaultId { - return nil, nil, secure, errors.New("Wrong vault") + return nil, nil, nil, secure, errors.New("Wrong vault") } // compute HMAC @@ -579,6 +579,7 @@ func (c *Cryptor) Decrypt(in []byte, user string) (resp []byte, names []string, aesCBC.CryptBlocks(clearData, encrypted.Data) resp, err = padding.RemovePadding(clearData) + labels = encrypted.Labels return } @@ -637,7 +638,6 @@ func (c *Cryptor) GetOwners(in []byte) (names []string, predicate string, err er addedNames[name] = true } } - predicate = encrypted.Predicate return diff --git a/cryptor/cryptor_test.go b/cryptor/cryptor_test.go index 4b744ac..fa4727c 100644 --- a/cryptor/cryptor_test.go +++ b/cryptor/cryptor_test.go @@ -83,7 +83,6 @@ func TestDuplicates(t *testing.T) { if err != nil { t.Fatalf("%v", err) } - c := Cryptor{&records, &cache} for _, name := range names { @@ -113,7 +112,7 @@ func TestDuplicates(t *testing.T) { t.Fatalf("%v", err) } - _, _, _, err := c.Decrypt(resp, name) + _, _, _, _, err := c.Decrypt(resp, name) if err == nil { t.Fatalf("That shouldn't have worked!") } diff --git a/hipchat/hipchat.go b/hipchat/hipchat.go new file mode 100644 index 0000000..f6451f4 --- /dev/null +++ b/hipchat/hipchat.go @@ -0,0 +1,67 @@ +package hipchat + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "strconv" +) + +const ( + RedBackground = "red" + YellowBackground = "yellow" + GreenBackground = "green" + GrayBackground = "gray" + RandomBackground = "random" + PurpleBackground = "purple" +) + +type HipchatClient struct { + RoomId int + ApiKey string + HcHost string + RoHost string +} + +func NewClient() *HipchatClient { + return &HipchatClient{} +} +func (h *HipchatClient) Notify(msg, color string) error { + if h.ApiKey == "" { + return errors.New("ApiKey unset") + } + msgBody := map[string]interface{}{ + "message": msg, + "notify": "true", + "color": color, + } + + body, err := json.Marshal(msgBody) + if err != nil { + return err + } + roomId := url.QueryEscape(strconv.Itoa(h.RoomId)) + hipchatUrl := fmt.Sprintf("https://%s/v2/room/%s/notification?auth_token=%s", h.HcHost, roomId, h.ApiKey) + req, err := http.NewRequest("POST", hipchatUrl, bytes.NewReader(body)) + + req.Header.Add("Content-Type", "application/json") + + Client := http.Client{} + resp, err := Client.Do(req) + if err != nil { + log.Printf("Could not post to hipchat for the reason %s", err.Error()) + return err + } + + _, err = ioutil.ReadAll(resp.Body) + if err != nil { + log.Printf("Could not post to hipchat for the reason %s", err.Error()) + return err + } + return nil +} diff --git a/index.html b/index.html index ea9ef7e..0d28049 100644 --- a/index.html +++ b/index.html @@ -27,6 +27,7 @@