diff --git a/core/core.go b/core/core.go index 0362703..cad4034 100644 --- a/core/core.go +++ b/core/core.go @@ -9,16 +9,22 @@ 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" ) var ( - crypt cryptor.Cryptor - records passvault.Records - cache keycache.Cache + crypt cryptor.Cryptor + records passvault.Records + cache keycache.Cache + orders order.Orderer + alternateName string ) // Each of these structures corresponds to the JSON expected on the @@ -63,6 +69,7 @@ type PasswordRequest struct { Password string NewPassword string + HipchatName string } type EncryptRequest struct { @@ -105,6 +112,26 @@ 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 +} + // These structures map the JSON responses that will be sent from the API type ResponseData struct { @@ -145,6 +172,26 @@ func jsonResponse(resp []byte) ([]byte, error) { return json.Marshal(ResponseData{Status: "ok", Response: resp}) } +func getAltNameFromName(alt, name string) (altName string, found bool) { + if passwordRecord, ok := records.Passwords[name]; ok { + if altName, ok := passwordRecord.AltNames[alt]; ok { + return altName, true + } + } + return "", false +} +func getAltNamesFromNames(alt string, names []string) map[string]string { + altNames := make(map[string]string) + for _, name := range names { + altName, found := getAltNameFromName(alt, name) + if !found { + altName = name + } + altNames[name] = altName + } + return altNames +} + // validateUser checks that the username and password passed in are // correct. If admin is true, the user must be an admin as well. func validateUser(name, password string, admin bool) error { @@ -168,8 +215,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") @@ -182,23 +228,37 @@ 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) } + if hcKey != "" && hcRoom != "" && hcHost != "" { + alternateName = "HipchatName" + roomId, err := strconv.Atoi(hcRoom) + if err != nil { + return errors.New("core.init unable to use hipchat roomId provided") + } + orders.Hipchat = hipchat.HipchatClient{ + ApiKey: hcKey, + RoomId: roomId, + HcHost: hcHost, + RoHost: roHost, + } + } cache = keycache.Cache{UserKeys: make(map[keycache.DelegateIndex]keycache.ActiveUser)} - crypt = cryptor.New(&records, &cache) + orders.PrepareOrders() + crypt = cryptor.New(&records, &cache, &orders) return err } @@ -351,6 +411,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() } @@ -422,7 +498,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) } @@ -488,7 +564,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) } @@ -532,7 +608,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) } @@ -548,6 +624,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) } @@ -658,3 +741,128 @@ 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 := getAltNamesFromNames(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 +} diff --git a/cryptor/cryptor.go b/cryptor/cryptor.go index fbdf556..26ed154 100644 --- a/cryptor/cryptor.go +++ b/cryptor/cryptor.go @@ -17,6 +17,7 @@ import ( "github.com/cloudflare/redoctober/keycache" "github.com/cloudflare/redoctober/msp" + "github.com/cloudflare/redoctober/order" "github.com/cloudflare/redoctober/padding" "github.com/cloudflare/redoctober/passvault" "github.com/cloudflare/redoctober/symcrypt" @@ -29,10 +30,11 @@ const ( type Cryptor struct { records *passvault.Records cache *keycache.Cache + orders *order.Orderer } -func New(records *passvault.Records, cache *keycache.Cache) Cryptor { - return Cryptor{records, cache} +func New(records *passvault.Records, cache *keycache.Cache, orders *order.Orderer) Cryptor { + return Cryptor{records, cache, orders} } // AccessStructure represents different possible access structures for @@ -508,14 +510,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 @@ -535,7 +537,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 @@ -563,6 +565,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 } @@ -621,7 +624,6 @@ func (c *Cryptor) GetOwners(in []byte) (names []string, predicate string, err er addedNames[name] = true } } - predicate = encrypted.Predicate return diff --git a/hipchat/hipchat.go b/hipchat/hipchat.go new file mode 100644 index 0000000..0d1ddc2 --- /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, + "color": color, + "notify": true, + } + + 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..a0c3f46 100644 --- a/index.html +++ b/index.html @@ -164,7 +164,7 @@
-

Change password

+

Change account

@@ -172,16 +172,18 @@
- +
- +
- - + + + +
@@ -309,6 +311,93 @@
+
+
+
+

Create Order

+ +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+ +
+
+
+
+
+
+

Order Info

+ +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+
+
+

Outstanding Orders

+ +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+