diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8abc69b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +bin/ +pkg/ +src/code.google.com/ +*~ diff --git a/Makefile b/Makefile index 5d1bc50..c41624e 100644 --- a/Makefile +++ b/Makefile @@ -3,17 +3,16 @@ VERSION := 0.1 ITERATION := $(shell date +%s) REVISION := $(shell git log -n1 --pretty=format:%h) -GOPATH := '$(CURDIR)' +export GOPATH := $(PWD) BUILD_DEPS := go .PHONY: all all: $(NAME) - .PHONY: test test: - GOPATH=$(GOPATH) go test $(NAME)/... + @go test $(NAME)/... .PHONY: print-builddeps print-builddeps: @@ -24,9 +23,8 @@ $(NAME): bin/$(NAME) SRC := $(shell find src/$(NAME) -type f) bin/$(NAME): $(SRC) - GOPATH=$(GOPATH) go install -tags "$(TAGS)" -ldflags "$(LDFLAGS)" $(NAME) - - + @go fmt $(NAME) + @go install -tags "$(TAGS)" -ldflags "$(LDFLAGS)" $(NAME) BUILD_PATH := build INSTALL_PREFIX := usr/local @@ -59,5 +57,7 @@ clean-package: .PHONY: clean clean: clean-package - GOPATH=$(GOPATH) go clean -i $(NAME)/... - $(RM) -r pkg + @go clean -i $(NAME)/... + @$(RM) -r pkg + +print-%: ; @echo $*=$($*) diff --git a/src/redoctober/core/core.go b/src/redoctober/core/core.go index 4c05d8d..0c09034 100644 --- a/src/redoctober/core/core.go +++ b/src/redoctober/core/core.go @@ -1,317 +1,310 @@ -// Pacakge core handles the main operations of the Red October server. +// Package core handles the main operations of the Red October server. +// +// Copyright (c) 2013 CloudFlare, Inc. + package core import ( - "log" - "errors" "encoding/json" - "redoctober/passvault" + "errors" + "fmt" + "log" "redoctober/cryptor" "redoctober/keycache" + "redoctober/passvault" ) -// format of incoming sign-in request +// Each of these structures corresponds to the JSON expected on the +// correspondingly named URI (e.g. the delegate structure maps to the +// JSON that should be sent on the /delegate URI and it is handled by +// the Delegate function below). + type create struct { - Name string + Name string Password string } type summary struct { - Name string + Name string Password string } type delegate struct { - Name string + Name string Password string - Uses int - Time string + + Uses int + Time string } type password struct { - Name string + Name string Password string + NewPassword string } type encrypt struct { - Name string + Name string Password string - Minimum int - Owners []string - Data []byte + + Minimum int + Owners []string + Data []byte } type decrypt struct { - Name string + Name string Password string - Data []byte + + Data []byte } type modify struct { - Name string + Name string Password string + ToModify string - Command string + Command string } -// response JSON format +// These structures map the JSON responses that will be sent from the API + type status struct { Status string } type responseData struct { - Status string + Status string Response []byte } type summaryData struct { Status string - Live map[string]keycache.ActiveUser - All map[string]passvault.Summary + Live map[string]keycache.ActiveUser + All map[string]passvault.Summary } -func errToJson(err error) (ret []byte) { - if err == nil { - ret, _ = json.Marshal(status{Status:"ok"}) - } else { - ret, _ = json.Marshal(status{Status:err.Error()}) - } - return +// Helper functions that create JSON responses sent by core + +func jsonStatusOk() ([]byte, error) { + return json.Marshal(status{Status: "ok"}) +} +func jsonStatusError(err error) ([]byte, error) { + return json.Marshal(status{Status: err.Error()}) +} +func jsonSummary() ([]byte, error) { + return json.Marshal(summaryData{Status: "ok",Live: keycache.GetSummary(), All: passvault.GetSummary()}) +} +func jsonResponse(resp []byte) ([]byte, error) { + return json.Marshal(responseData{Status: "ok", Response: resp}) } -func summaryToJson(err error) (ret []byte) { - if err == nil { - ret, _ = json.Marshal(summaryData{Status:"ok", Live:keycache.GetSummary(), All:passvault.GetSummary()}) - } else { - ret, _ = json.Marshal(status{Status:err.Error()}) - } - return -} - -func responseToJson(resp []byte, err error) (ret []byte) { - if err == nil { - ret, _ = json.Marshal(responseData{Status:"ok", Response:resp}) - } else { - ret, _ = json.Marshal(status{Status:err.Error()}) - } - return -} - -func validateAdmin(name string, password string) (err error) { +// validateAdmin checks that the username and password passed in are +// correct and that the user is an admin +func validateAdmin(name, password string) error { if passvault.NumRecords() == 0 { return errors.New("Vault is not created yet") } - // find record - passwordRec, ok := passvault.GetRecord(name) + pr, ok := passvault.GetRecord(name) if !ok { return errors.New("User not present") } - err = passwordRec.ValidatePassword(password) - if err != nil { - return + if err := pr.ValidatePassword(password); err != nil { + return err } - if !passwordRec.IsAdmin() { + if !pr.IsAdmin() { return errors.New("Admin required") } + return nil +} + +// Init reads the records from disk from a given path +func Init(path string) (err error) { + if err = passvault.InitFromDisk(path); err != nil { + err = fmt.Errorf("Failed to load password vault %s: %s", path, err) + } return } -// Init reads the records from disk from a given path. -func Init(path string) { - passvault.InitFromDisk(path) -} - // Create processes a create request. -func Create(jsonIn []byte) []byte { +func Create(jsonIn []byte) ([]byte, error) { var s create - err := json.Unmarshal(jsonIn, &s) - if err != nil { - return errToJson(err) + if err := json.Unmarshal(jsonIn, &s); err != nil { + return jsonStatusError(err) } if passvault.NumRecords() != 0 { - return errToJson(errors.New("Vault is already created")) + return jsonStatusError(errors.New("Vault is already created")) + } + + if _, err := passvault.AddNewRecord(s.Name, s.Password, true); err != nil { + log.Printf("Error adding record for %s: %s\n", s.Name, err) + return jsonStatusError(err) } - _, err = passvault.AddNewRecord(s.Name, s.Password, true) - if err != nil { - log.Println("Error adding record:", err) - return errToJson(err) - } - - return errToJson(err) + return jsonStatusOk() } // Summary processes a summary request. -func Summary(jsonIn []byte) []byte { +func Summary(jsonIn []byte) ([]byte, error) { var s summary keycache.Refresh() - err := json.Unmarshal(jsonIn, &s) - if err != nil { - return errToJson(err) + if err := json.Unmarshal(jsonIn, &s); err != nil { + return jsonStatusError(err) } if passvault.NumRecords() == 0 { - return errToJson(errors.New("Vault is not created yet")) + return jsonStatusError(errors.New("Vault is not created yet")) } - // validate admin - err = validateAdmin(s.Name, s.Password) - if err != nil { - log.Println("Error validating admin status", err) - return errToJson(err) + if err := validateAdmin(s.Name, s.Password); err != nil { + log.Printf("Error validating admin status of %s: %s", s.Name, err) + return jsonStatusError(err) } - // populate - return summaryToJson(err) + return jsonSummary() } // Delegate processes a delegation request. -func Delegate(jsonIn []byte) []byte { +func Delegate(jsonIn []byte) ([]byte, error) { var s delegate - err := json.Unmarshal(jsonIn, &s) - if err != nil { - return errToJson(err) + if err := json.Unmarshal(jsonIn, &s); err != nil { + return jsonStatusError(err) } if passvault.NumRecords() == 0 { - return errToJson(errors.New("Vault is not created yet")) + return jsonStatusError(errors.New("Vault is not created yet")) } - // find record - passwordRec, ok := passvault.GetRecord(s.Name) - if ok { - err = passwordRec.ValidatePassword(s.Password) - if err != nil { - return errToJson(err) + // Find password record for user and verify that their password + // matches. If not found then add a new entry for this user. + + pr, found := passvault.GetRecord(s.Name) + if found { + if err := pr.ValidatePassword(s.Password); err != nil { + return jsonStatusError(err) } } else { - passwordRec, err = passvault.AddNewRecord(s.Name, s.Password, false) - if err != nil { - log.Println("Error adding record:", err) - return errToJson(err) + var err error + if pr, err = passvault.AddNewRecord(s.Name, s.Password, false); err != nil { + log.Printf("Error adding record for %s: %s\n", s.Name, err) + return jsonStatusError(err) } } // add signed-in record to active set - err = keycache.AddKeyFromRecord(passwordRec, s.Name, s.Password, s.Uses, s.Time) - if err != nil { - log.Println("Error adding key to cache:", err) - return errToJson(err) + if err := keycache.AddKeyFromRecord(pr, s.Name, s.Password, s.Uses, s.Time); err != nil { + log.Printf("Error adding key to cache for %s: %s\n", s.Name, err) + return jsonStatusError(err) } - return errToJson(err) + return jsonStatusOk() } // Password processes a password change request. -func Password(jsonIn []byte) []byte { +func Password(jsonIn []byte) ([]byte, error) { var s password - err := json.Unmarshal(jsonIn, &s) - if err != nil { - return errToJson(err) + if err := json.Unmarshal(jsonIn, &s); err != nil { + return jsonStatusError(err) } if passvault.NumRecords() == 0 { - return errToJson(errors.New("Vault is not created yet")) + return jsonStatusError(errors.New("Vault is not created yet")) } // add signed-in record to active set - err = passvault.ChangePassword(s.Name, s.Password, s.NewPassword) - if err != nil { + if err := passvault.ChangePassword(s.Name, s.Password, s.NewPassword); err != nil { log.Println("Error changing password:", err) - return errToJson(err) + return jsonStatusError(err) } - return errToJson(err) + return jsonStatusOk() } // Encrypt processes an encrypt request. -func Encrypt(jsonIn []byte) (ret []byte) { +func Encrypt(jsonIn []byte) ([]byte, error) { var s encrypt - err := json.Unmarshal(jsonIn, &s) - if err != nil { - return errToJson(err) + if err := json.Unmarshal(jsonIn, &s); err != nil { + return jsonStatusError(err) } - err = validateAdmin(s.Name, s.Password) - if err != nil { + if err := validateAdmin(s.Name, s.Password); err != nil { log.Println("Error validating admin status", err) - return errToJson(err) + return jsonStatusError(err) } // Encrypt file with list of owners - resp, err := cryptor.Encrypt(s.Data, s.Owners, s.Minimum) - if err != nil { + if resp, err := cryptor.Encrypt(s.Data, s.Owners, s.Minimum); err != nil { log.Println("Error encrypting:", err) - return errToJson(err) + return jsonStatusError(err) + } else { + return jsonResponse(resp) } - - return responseToJson(resp, err) } // Decrypt processes a decrypt request. -func Decrypt(jsonIn []byte) (ret []byte) { +func Decrypt(jsonIn []byte) ([]byte, error) { var s decrypt err := json.Unmarshal(jsonIn, &s) if err != nil { - return errToJson(err) + return jsonStatusError(err) } err = validateAdmin(s.Name, s.Password) if err != nil { log.Println("Error validating admin status", err) - return errToJson(err) + return jsonStatusError(err) } resp, err := cryptor.Decrypt(s.Data) if err != nil { log.Println("Error decrypting:", err) - return errToJson(err) + return jsonStatusError(err) } - return responseToJson(resp, err) + return jsonResponse(resp) } // Modify processes a modify request. -func Modify(jsonIn []byte) []byte { +func Modify(jsonIn []byte) ([]byte, error) { var s modify - err := json.Unmarshal(jsonIn, &s) - if err != nil { - return errToJson(err) + if err := json.Unmarshal(jsonIn, &s); err != nil { + return jsonStatusError(err) } - err = validateAdmin(s.Name, s.Password) - if err != nil { - log.Println("Error validating admin status", err) - return errToJson(err) + if err := validateAdmin(s.Name, s.Password); err != nil { + log.Printf("Error validating admin status of %s: %s", s.Name, err) + return jsonStatusError(err) } if _, ok := passvault.GetRecord(s.ToModify); !ok { - return errToJson(errors.New("Record to modify missing")) + return jsonStatusError(errors.New("Record to modify missing")) } if s.Name == s.ToModify { - return errToJson(errors.New("Cannot modify own record")) + return jsonStatusError(errors.New("Cannot modify own record")) } - switch s.Command { - case "delete": { - err = passvault.DeleteRecord(s.ToModify) - } - case "revoke": { - err = passvault.RevokeRecord(s.ToModify) - } - case "admin": { - err = passvault.MakeAdmin(s.ToModify) - } - default: { - return errToJson(errors.New("Unknown command")) - } - } - return errToJson(err) -} + var err error + switch s.Command { + case "delete": + err = passvault.DeleteRecord(s.ToModify) + case "revoke": + err = passvault.RevokeRecord(s.ToModify) + case "admin": + err = passvault.MakeAdmin(s.ToModify) + default: + return jsonStatusError(errors.New("Unknown command")) + } + + if err != nil { + return jsonStatusError(err) + } else { + return jsonStatusOk() + } +} diff --git a/src/redoctober/core/core_test.go b/src/redoctober/core/core_test.go index aac5e71..149f9f3 100644 --- a/src/redoctober/core/core_test.go +++ b/src/redoctober/core/core_test.go @@ -1,23 +1,32 @@ +// core_test.go: tests for core.go +// +// Copyright (c) 2013 CloudFlare, Inc. + package core import ( - "os" "encoding/json" "testing" + "os" "redoctober/passvault" "redoctober/keycache" + "redoctober/keycache" + "redoctober/passvault" ) func TestCreate(t *testing.T) { createJson := []byte("{\"Name\":\"Alice\",\"Password\":\"Hello\"}") - + os.Remove("/tmp/db1.json") Init("/tmp/db1.json") - respJson := Create(createJson) + respJson, err := Create(createJson) + if err != nil { + t.Fatalf("Error in creating account, ", err) + } var s responseData - err := json.Unmarshal(respJson, &s) + err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in creating account, ", err) } @@ -26,7 +35,10 @@ func TestCreate(t *testing.T) { } // check to see if creation can happen twice - respJson = Create(createJson) + respJson, err = Create(createJson) + if err != nil { + t.Fatalf("Error in creating account when one exists, ", err) + } err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in creating account when one exists, ", err) @@ -44,9 +56,12 @@ func TestSummary(t *testing.T) { os.Remove("/tmp/db1.json") // check for summary of uninitialized vault - respJson := Summary(createJson) + respJson, err := Summary(createJson) + if err != nil { + t.Fatalf("Error in summary of account with no vault,", err) + } var s summaryData - err := json.Unmarshal(respJson, &s) + err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in summary of account with no vault,", err) } @@ -57,7 +72,10 @@ func TestSummary(t *testing.T) { Init("/tmp/db1.json") // check for summary of initialized vault - respJson = Create(createJson) + respJson, err = Create(createJson) + if err != nil { + t.Fatalf("Error in creating account, ", err) + } err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in creating account, ", err) @@ -66,7 +84,10 @@ func TestSummary(t *testing.T) { t.Fatalf("Error in creating account, ", s.Status) } - respJson = Summary(createJson) + respJson, err = Summary(createJson) + if err != nil { + t.Fatalf("Error in summary of account,", err) + } err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in summary of account,", err) @@ -87,7 +108,10 @@ func TestSummary(t *testing.T) { } // check for summary of initialized vault with new member - respJson = Delegate(delegateJson) + respJson, err = Delegate(delegateJson) + if err != nil { + t.Fatalf("Error in delegating account,", err) + } err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in delegating account,", err) @@ -96,7 +120,10 @@ func TestSummary(t *testing.T) { t.Fatalf("Error in delegating account, ", s.Status) } - respJson = Summary(createJson) + respJson, err = Summary(createJson) + if err != nil { + t.Fatalf("Error in summary of account with no vault,", err) + } err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in summary of account with no vault,", err) @@ -138,7 +165,6 @@ func TestSummary(t *testing.T) { t.Fatalf("Error in summary of account, record missing ") } - // keycache.FlushCache() os.Remove("/tmp/db1.json") @@ -156,8 +182,11 @@ func TestPassword(t *testing.T) { // check for summary of initialized vault with new member var s responseData - respJson := Create(createJson) - err := json.Unmarshal(respJson, &s) + respJson, err := Create(createJson) + if err != nil { + t.Fatalf("Error in creating account, ", err) + } + err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in creating account, ", err) } @@ -165,7 +194,10 @@ func TestPassword(t *testing.T) { t.Fatalf("Error in creating account, ", s.Status) } - respJson = Delegate(delegateJson) + respJson, err = Delegate(delegateJson) + if err != nil { + t.Fatalf("Error in delegating account,", err) + } err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in delegating account,", err) @@ -174,7 +206,10 @@ func TestPassword(t *testing.T) { t.Fatalf("Error in delegating account, ", s.Status) } - respJson = Password(passwordJson2) + respJson, err = Password(passwordJson2) + if err != nil { + t.Fatalf("Error in password", err) + } err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in password", err) @@ -183,7 +218,10 @@ func TestPassword(t *testing.T) { t.Fatalf("Error in password, ", s.Status) } - respJson = Password(passwordJson) + respJson, err = Password(passwordJson) + if err != nil { + t.Fatalf("Error in password", err) + } err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in password", err) @@ -192,7 +230,10 @@ func TestPassword(t *testing.T) { t.Fatalf("Error in password, ", s.Status) } - respJson = Delegate(delegateJson) + respJson, err = Delegate(delegateJson) + if err != nil { + t.Fatalf("Error in delegating account,", err) + } err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in delegating account,", err) @@ -201,7 +242,10 @@ func TestPassword(t *testing.T) { t.Fatalf("Error in delegating account, ", s.Status) } - respJson = Delegate(delegateJson2) + respJson, err = Delegate(delegateJson2) + if err != nil { + t.Fatalf("Error in delegating account,", err) + } err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in delegating account,", err) @@ -210,7 +254,10 @@ func TestPassword(t *testing.T) { t.Fatalf("Error in delegating account, ", s.Status) } - respJson = Password(passwordJson2) + respJson, err = Password(passwordJson2) + if err != nil { + t.Fatalf("Error in password", err) + } err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in password", err) @@ -219,7 +266,10 @@ func TestPassword(t *testing.T) { t.Fatalf("Error in password, ", s.Status) } - respJson = Delegate(delegateJson) + respJson, err = Delegate(delegateJson) + if err != nil { + t.Fatalf("Error in delegating account,", err) + } err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in delegating account,", err) @@ -248,16 +298,22 @@ func TestEncryptDecrypt(t *testing.T) { // check for summary of initialized vault with new member var s responseData - respJson := Create(delegateJson) - err := json.Unmarshal(respJson, &s) + respJson, err := Create(delegateJson) + if err != nil { + t.Fatalf("Error in creating account,", err) + } + err = json.Unmarshal(respJson, &s) + if err != nil { + t.Fatalf("Error in creating account,", err) + } + if s.Status != "ok" { + t.Fatalf("Error in creating account, ", s.Status) + } + + respJson, err = Delegate(delegateJson2) if err != nil { t.Fatalf("Error in delegating account,", err) } - if s.Status != "ok" { - t.Fatalf("Error in delegating account, ", s.Status) - } - - respJson = Delegate(delegateJson2) err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in delegating account,", err) @@ -266,7 +322,10 @@ func TestEncryptDecrypt(t *testing.T) { t.Fatalf("Error in delegating account, ", s.Status) } - respJson = Delegate(delegateJson3) + respJson, err = Delegate(delegateJson3) + if err != nil { + t.Fatalf("Error in delegating account,", err) + } err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in delegating account,", err) @@ -277,7 +336,10 @@ func TestEncryptDecrypt(t *testing.T) { // check summary to see if none are delegated keycache.Refresh() - respJson = Summary(summaryJson) + respJson, err = Summary(summaryJson) + if err != nil { + t.Fatalf("Error in summary,", err) + } var sum summaryData err = json.Unmarshal(respJson, &sum) if err != nil { @@ -291,7 +353,10 @@ func TestEncryptDecrypt(t *testing.T) { } // Encrypt with non-admin (fail) - respJson = Encrypt(encryptJson) + respJson, err = Encrypt(encryptJson) + if err != nil { + t.Fatalf("Error in encrypt,", err) + } err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in encrypt,", err) @@ -301,7 +366,10 @@ func TestEncryptDecrypt(t *testing.T) { } // Encrypt - respJson = Encrypt(encryptJson2) + respJson, err = Encrypt(encryptJson2) + if err != nil { + t.Fatalf("Error in encrypt,", err) + } err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in encrypt,", err) @@ -310,14 +378,16 @@ func TestEncryptDecrypt(t *testing.T) { t.Fatalf("Error in encrypt, ", s.Status) } - // decrypt file decryptJson, err := json.Marshal(decrypt{Name:"Alice", Password:"Hello", Data:s.Response}) if err != nil { t.Fatalf("Error in marshalling decryption,", err) } - respJson2 := Decrypt(decryptJson) + respJson2, err := Decrypt(decryptJson) + if err != nil { + t.Fatalf("Error in decrypt,", err) + } err = json.Unmarshal(respJson2, &s) if err != nil { t.Fatalf("Error in decrypt,", err) @@ -327,7 +397,10 @@ func TestEncryptDecrypt(t *testing.T) { } // delegate two valid decryptors - respJson = Delegate(delegateJson4) + respJson, err = Delegate(delegateJson4) + if err != nil { + t.Fatalf("Error in delegating account,", err) + } err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in delegating account,", err) @@ -336,7 +409,10 @@ func TestEncryptDecrypt(t *testing.T) { t.Fatalf("Error in delegating account, ", s.Status) } - respJson = Delegate(delegateJson5) + respJson, err = Delegate(delegateJson5) + if err != nil { + t.Fatalf("Error in delegating account,", err) + } err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in delegating account,", err) @@ -348,7 +424,10 @@ func TestEncryptDecrypt(t *testing.T) { // verify the presence of the two delgations keycache.Refresh() var sum2 summaryData - respJson = Summary(summaryJson) + respJson, err = Summary(summaryJson) + if err != nil { + t.Fatalf("Error in summary,", err) + } err = json.Unmarshal(respJson, &sum2) if err != nil { t.Fatalf("Error in summary,", err) @@ -360,7 +439,10 @@ func TestEncryptDecrypt(t *testing.T) { t.Fatalf("Error in summary, ", sum2.Live) } - respJson2 = Decrypt(decryptJson) + respJson2, err = Decrypt(decryptJson) + if err != nil { + t.Fatalf("Error in decrypt,", err) + } err = json.Unmarshal(respJson2, &s) if err != nil { t.Fatalf("Error in decrypt,", err) @@ -394,16 +476,22 @@ func TestModify(t *testing.T) { // check for summary of initialized vault with new member var s responseData - respJson := Create(delegateJson) - err := json.Unmarshal(respJson, &s) + respJson, err := Create(delegateJson) + if err != nil { + t.Fatalf("Error in creating account,", err) + } + err = json.Unmarshal(respJson, &s) + if err != nil { + t.Fatalf("Error in creating account,", err) + } + if s.Status != "ok" { + t.Fatalf("Error in creating account, ", s.Status) + } + + respJson, err = Delegate(delegateJson2) if err != nil { t.Fatalf("Error in delegating account,", err) } - if s.Status != "ok" { - t.Fatalf("Error in delegating account, ", s.Status) - } - - respJson = Delegate(delegateJson2) err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in delegating account,", err) @@ -412,7 +500,10 @@ func TestModify(t *testing.T) { t.Fatalf("Error in delegating account, ", s.Status) } - respJson = Delegate(delegateJson3) + respJson, err = Delegate(delegateJson3) + if err != nil { + t.Fatalf("Error in delegating account,", err) + } err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in delegating account,", err) @@ -423,7 +514,10 @@ func TestModify(t *testing.T) { // check summary to see if none are delegated keycache.Refresh() - respJson = Summary(summaryJson) + respJson, err = Summary(summaryJson) + if err != nil { + t.Fatalf("Error in summary,", err) + } var sum summaryData err = json.Unmarshal(respJson, &sum) if err != nil { @@ -437,7 +531,10 @@ func TestModify(t *testing.T) { } // Modify from non-admin (fail) - respJson = Modify(modifyJson) + respJson, err = Modify(modifyJson) + if err != nil { + t.Fatalf("Error in modify,", err) + } err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in modify,", err) @@ -447,7 +544,10 @@ func TestModify(t *testing.T) { } // Modify self from admin (fail) - respJson = Modify(modifyJson2) + respJson, err = Modify(modifyJson2) + if err != nil { + t.Fatalf("Error in modify,", err) + } err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in modify,", err) @@ -457,7 +557,10 @@ func TestModify(t *testing.T) { } // Modify admin from admin - respJson = Modify(modifyJson3) + respJson, err = Modify(modifyJson3) + if err != nil { + t.Fatalf("Error in modify,", err) + } err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in modify,", err) @@ -466,7 +569,10 @@ func TestModify(t *testing.T) { t.Fatalf("Error in modify, ", s.Status) } - respJson = Summary(summaryJson) + respJson, err = Summary(summaryJson) + if err != nil { + t.Fatalf("Error in summary,", err) + } err = json.Unmarshal(respJson, &sum) if err != nil { t.Fatalf("Error in summary,", err) @@ -479,7 +585,10 @@ func TestModify(t *testing.T) { } // Revoke admin from admin - respJson = Modify(modifyJson4) + respJson, err = Modify(modifyJson4) + if err != nil { + t.Fatalf("Error in modify,", err) + } err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in modify,", err) @@ -488,7 +597,10 @@ func TestModify(t *testing.T) { t.Fatalf("Error in modify, ", s.Status) } - respJson = Summary(summaryJson2) + respJson, err = Summary(summaryJson2) + if err != nil { + t.Fatalf("Error in summary,", err) + } err = json.Unmarshal(respJson, &sum) if err != nil { t.Fatalf("Error in summary,", err) @@ -501,7 +613,10 @@ func TestModify(t *testing.T) { } // Delete from admin - respJson = Modify(modifyJson5) + respJson, err = Modify(modifyJson5) + if err != nil { + t.Fatalf("Error in modify,", err) + } err = json.Unmarshal(respJson, &s) if err != nil { t.Fatalf("Error in modify,", err) @@ -511,7 +626,10 @@ func TestModify(t *testing.T) { } var sum3 summaryData - respJson = Summary(summaryJson2) + respJson, err = Summary(summaryJson2) + if err != nil { + t.Fatalf("Error in summary,", err) + } err = json.Unmarshal(respJson, &sum3) if err != nil { t.Fatalf("Error in summary,", err) @@ -527,4 +645,3 @@ func TestModify(t *testing.T) { os.Remove("/tmp/db1.json") } - diff --git a/src/redoctober/cryptor/cryptor.go b/src/redoctober/cryptor/cryptor.go index 342fc48..4421564 100644 --- a/src/redoctober/cryptor/cryptor.go +++ b/src/redoctober/cryptor/cryptor.go @@ -1,47 +1,51 @@ -// Package cryptor encrypts and decrypts files using the Red October vault and key cache. +// Package cryptor encrypts and decrypts files using the Red October +// vault and key cache. +// +// Copyright (c) 2013 CloudFlare, Inc. + package cryptor import ( "crypto/aes" - "crypto/hmac" - "crypto/sha1" - "crypto/rand" "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "crypto/sha1" "encoding/json" "errors" - "redoctober/passvault" "redoctober/keycache" "redoctober/padding" + "redoctober/passvault" ) const ( DEFAULT_VERSION = 1 ) -// MultiWrappedKey is a structure containing -// a 16-byte key encrypted once for each of the keys corresponding to -// the names of the users in Name in order. +// MultiWrappedKey is a structure containing a 16-byte key encrypted +// once for each of the keys corresponding to the names of the users +// in Name in order. type MultiWrappedKey struct { Name []string - Key []byte + Key []byte } -// SingleWrappedKey is a structure containing -// a 16-byte key encrypted by an RSA key. +// SingleWrappedKey is a structure containing a 16-byte key encrypted +// by an RSA key. type SingleWrappedKey struct { - Key []byte + Key []byte aesKey []byte } -// EncryptedFile is the format for encrypted data containing all the keys necessary to -// decrypt it when delegated. +// EncryptedFile is the format for encrypted data containing all the +// keys necessary to decrypt it when delegated. type EncryptedFile struct { - Version int - VaultId int - KeySet []MultiWrappedKey + Version int + VaultId int + KeySet []MultiWrappedKey KeySetRSA map[string]SingleWrappedKey - IV []byte - Data []byte + IV []byte + Data []byte Signature []byte } @@ -56,7 +60,8 @@ func makeRandom(length int) (bytes []byte, err error) { return } -// encrypt clearKey with the key associated with name inner, then name outer +// encrypt clearKey with the key associated with name inner, then name +// outer func encryptKey(nameInner, nameOuter string, clearKey []byte, rsaKeys map[string]SingleWrappedKey) (out MultiWrappedKey, err error) { out.Name = []string{nameOuter, nameInner} @@ -90,7 +95,7 @@ func encryptKey(nameInner, nameOuter string, clearKey []byte, rsaKeys map[string err = errors.New("Missing user in file") return } - + overrideOuter, ok = rsaKeys[nameOuter] if !ok { err = errors.New("Missing user in file") @@ -105,12 +110,10 @@ func encryptKey(nameInner, nameOuter string, clearKey []byte, rsaKeys map[string } // double-wrap the keys - keyBytes, err = keycache.EncryptKey(clearKey, nameInner, overrideInner.aesKey) - if err != nil { + if keyBytes, err = keycache.EncryptKey(clearKey, nameInner, overrideInner.aesKey); err != nil { return out, err } - keyBytes, err = keycache.EncryptKey(keyBytes, nameOuter, overrideOuter.aesKey) - if err != nil { + if keyBytes, err = keycache.EncryptKey(keyBytes, nameOuter, overrideOuter.aesKey); err != nil { return out, err } @@ -122,7 +125,7 @@ func encryptKey(nameInner, nameOuter string, clearKey []byte, rsaKeys map[string // decrypt first key in keys whose encryption keys are in keycache func unwrapKey(keys []MultiWrappedKey, rsaKeys map[string]SingleWrappedKey) (unwrappedKey []byte, err error) { var ( - keyFound error + keyFound error fullMatch bool = false ) for _, mwKey := range keys { @@ -153,22 +156,21 @@ func unwrapKey(keys []MultiWrappedKey, rsaKeys map[string]SingleWrappedKey) (unw return } - // Encrypt encrypts data with the keys associated with names // This requires a minimum of min keys to decrypt. // NOTE: as currently implemented, the maximum value for min is 2. func Encrypt(in []byte, names []string, min int) (resp []byte, err error) { - // decode data to encrypt - clearFile := padding.PadClearFile(in) if min > 2 { return nil, errors.New("Minimum restricted to 2") } + // decode data to encrypt + clearFile := padding.AddPadding(in) + // set up encrypted data structure var encrypted EncryptedFile encrypted.Version = DEFAULT_VERSION - encrypted.VaultId, err = passvault.GetVaultId() - if err != nil { + if encrypted.VaultId, err = passvault.GetVaultId(); err != nil { return } @@ -182,14 +184,14 @@ func Encrypt(in []byte, names []string, min int) (resp []byte, err error) { if err != nil { return } - + // allocate set of keys to be able to cover all ordered subsets // of length 2 of names encrypted.KeySet = make([]MultiWrappedKey, len(names)*(len(names)-1)) // create map to hold RSA encrypted keys encrypted.KeySetRSA = make(map[string]SingleWrappedKey) - + var singleWrappedKey SingleWrappedKey for _, name := range names { rec, ok := passvault.GetRecord(name) @@ -257,8 +259,7 @@ func Encrypt(in []byte, names []string, min int) (resp []byte, err error) { func Decrypt(in []byte) (resp []byte, err error) { // unwrap encrypted file var encrypted EncryptedFile - err = json.Unmarshal(in, &encrypted) - if err != nil { + if err = json.Unmarshal(in, &encrypted); err != nil { return } if encrypted.Version != DEFAULT_VERSION { @@ -296,11 +297,10 @@ func Decrypt(in []byte) (resp []byte, err error) { // decrypt file key with delegate keys var unwrappedKey = make([]byte, 16) - unwrappedKey, err = unwrapKey(encrypted.KeySet, encrypted.KeySetRSA) - if err != nil { + if unwrappedKey, err = unwrapKey(encrypted.KeySet, encrypted.KeySetRSA); err != nil { return } - + // set up the decryption context aesCrypt, err := aes.NewCipher(unwrappedKey) if err != nil { @@ -314,4 +314,3 @@ func Decrypt(in []byte) (resp []byte, err error) { return padding.RemovePadding(clearData) } - diff --git a/src/redoctober/keycache/keycache.go b/src/redoctober/keycache/keycache.go index 6835c80..ece95e0 100644 --- a/src/redoctober/keycache/keycache.go +++ b/src/redoctober/keycache/keycache.go @@ -1,16 +1,19 @@ // Package keycache provides the ability to hold active keys in memory // for the Red October server. +// +// Copyright (c) 2013 CloudFlare, Inc. + package keycache import ( - "log" - "time" - "errors" "crypto/aes" + "crypto/rand" "crypto/rsa" "crypto/sha1" - "crypto/rand" + "errors" + "log" "redoctober/passvault" + "time" ) // UserKeys is the set of decrypted keys in memory, indexed by name. @@ -18,11 +21,11 @@ var UserKeys map[string]ActiveUser = make(map[string]ActiveUser) // ActiveUser holds the information about an actively delegated key type ActiveUser struct { - Admin bool - Type string + Admin bool + Type string Expiry time.Time - Uses int - // non-public members + Uses int + aesKey []byte rsaKey rsa.PrivateKey } @@ -38,8 +41,7 @@ func setUser(in ActiveUser, name string) { // mark a use of the key, only for decryption or symmetric encryption func useKey(name string) { - val, present := matchUser(name) - if present { + if val, present := matchUser(name); present { val.Uses -= 1 setUser(val, name) } @@ -68,7 +70,7 @@ func Refresh() { } // AddKeyFromRecord decrypts a key for a given record and adds it to the cache. -func AddKeyFromRecord(record passvault.DiskPasswordRecord, name string, password string, uses int, durationString string) (err error) { +func AddKeyFromRecord(record passvault.PasswordRecord, name string, password string, uses int, durationString string) (err error) { var current ActiveUser Refresh() @@ -184,4 +186,3 @@ func DecryptKey(in []byte, name string, rsaEncryptedKey []byte) (out []byte, err return } - diff --git a/src/redoctober/keycache/keycache_test.go b/src/redoctober/keycache/keycache_test.go index b960f89..3d978f2 100644 --- a/src/redoctober/keycache/keycache_test.go +++ b/src/redoctober/keycache/keycache_test.go @@ -1,9 +1,12 @@ +// keycache_test.go: tests for keycache.go +// +// Copyright (c) 2013 CloudFlare, Inc. package keycache import ( - "passvault" - "time" + "redoctober/passvault" "testing" + "time" ) var now = time.Now() @@ -13,10 +16,10 @@ var dummy = make([]byte, 16) func TestUsesFlush(t *testing.T) { singleUse := ActiveUser{ - Admin: true, - Type: passvault.AESRecord, + Admin: true, + Type: passvault.AESRecord, Expiry: nextYear, - Uses: 2, + Uses: 2, aesKey: emptyKey, } @@ -47,10 +50,10 @@ func TestTimeFlush(t *testing.T) { one := now.Add(oneSec) singleUse := ActiveUser{ - Admin: true, - Type: passvault.AESRecord, + Admin: true, + Type: passvault.AESRecord, Expiry: one, - Uses: 10, + Uses: 10, aesKey: emptyKey, } @@ -76,5 +79,3 @@ func TestTimeFlush(t *testing.T) { t.Fatalf("Error in pruning expired key") } } - - diff --git a/src/redoctober/padding/padding.go b/src/redoctober/padding/padding.go index da9de2a..bcbc6a3 100644 --- a/src/redoctober/padding/padding.go +++ b/src/redoctober/padding/padding.go @@ -1,29 +1,46 @@ // Package padding adds and removes padding for AES-CBC mode. +// +// Copyright (c) 2013 CloudFlare, Inc. + package padding import "errors" -// RemovePadding removes padding from clear data. -func RemovePadding(bytesPadded []byte) ([]byte, error) { - // last byte is padding byte - paddingLen := int(bytesPadded[len(bytesPadded) - 1]) - if paddingLen > 16 { +// The final byte of a padded []byte indicates the number of padding +// bytes that were added. The padding bytes are always NUL bytes and +// up to 16 bytes may be added. +// +// Examples: +// +// 1. Data to be padded has a length divisible by 16. 16 bytes will be +// added where the first 15 are 0x00 and the final byte is 0x10. +// +// 2. Data to be padded has a length with remainder 15 when divided by +// 16. One byte will be added and that byte will be 0x01 (indicating +// one byte of padding). +// +// 3. Data to be padded has a length with remainder 2 when divided by +// 16. 14 bytes will be added. The first 13 will be 0x00 and then final +// byte will be 0x0e. +// +// Removing padding is trivial: the number of bytes specified by the +// final byte are removed. + +// RemovePadding removes padding from data that was added with +// AddPadding +func RemovePadding(b []byte) ([]byte, error) { + l := int(b[len(b)-1]) + if l > 16 { return nil, errors.New("Padding incorrect") } - fileLen := len(bytesPadded) - paddingLen - return bytesPadded[:fileLen], nil + return b[:len(b)-l], nil } -// PadClearFile adds padding to clear file. -func PadClearFile(fileBytes []byte) (paddedFile []byte) { - // pad with zeros, last byte is the size of padding - paddingLen := 16 - len(fileBytes) % 16 - padding := make([]byte, paddingLen) - padding[paddingLen - 1] = byte(paddingLen) - paddedFile = append(fileBytes, padding...) - - return +// AddPadding adds padding to a block of data +func AddPadding(b []byte) []byte { + l := 16 - len(b)%16 + padding := make([]byte, l) + padding[l-1] = byte(l) + return append(b, padding...) } - - diff --git a/src/redoctober/padding/padding_test.go b/src/redoctober/padding/padding_test.go new file mode 100644 index 0000000..ee39209 --- /dev/null +++ b/src/redoctober/padding/padding_test.go @@ -0,0 +1,73 @@ +// padding_test.go: tests for padding.go +// +// Copyright (c) 2013 CloudFlare, Inc. + +package padding + +import ( + "bytes" + "testing" +) + +func assert(t *testing.T, b bool) { + if !b { + t.Fail() + } +} + +func TestAddPadding(t *testing.T) { + b := make([]byte, 16) + c := AddPadding(b) + assert(t, len(c) == 32) + assert(t, c[31] == 16) + + b = make([]byte, 32) + c = AddPadding(b) + assert(t, len(c) == 48) + assert(t, c[47] == 16) + + b = make([]byte, 1) + c = AddPadding(b) + assert(t, len(c) == 16) + assert(t, c[15] == 15) + + b = make([]byte, 15) + c = AddPadding(b) + assert(t, len(c) == 16) + assert(t, c[15] == 1) +} + +func TestRemovePadding(t *testing.T) { + b := []byte("0123456789ABCDEF") + c := AddPadding(b) + assert(t, len(c) == 32) + assert(t, c[31] == 16) + assert(t, bytes.Compare(c[:16], b[:16]) == 0) + d, err := RemovePadding(c) + assert(t, err == nil) + assert(t, len(d) == 16) + assert(t, bytes.Compare(b, d) == 0) + + b = []byte("0123456789") + c = AddPadding(b) + assert(t, len(c) == 16) + assert(t, c[15] == 6) + assert(t, bytes.Compare(c[:10], b[:10]) == 0) + d, err = RemovePadding(c) + assert(t, err == nil) + assert(t, len(d) == 10) + assert(t, bytes.Compare(b, d) == 0) +} + +func TestDetectBadPadding(t *testing.T) { + b := []byte("0123456789ABCDEF") + c := AddPadding(b) + assert(t, len(c) == 32) + assert(t, c[31] == 16) + assert(t, bytes.Compare(c[:16], b[:16]) == 0) + c[31] = 42 + d, err := RemovePadding(c) + assert(t, err != nil) + assert(t, d == nil) +} + diff --git a/src/redoctober/passvault/passvault.go b/src/redoctober/passvault/passvault.go index be8664c..85473a3 100644 --- a/src/redoctober/passvault/passvault.go +++ b/src/redoctober/passvault/passvault.go @@ -1,154 +1,151 @@ -// Package passvault manages the vault containing user records on disk. +// Package passvault manages the vault containing user records on +// disk. It contains usernames and associated passwords which are +// stored hashed (with salt) using scrypt. +// +// Copyright (c) 2013 CloudFlare, Inc. + package passvault import ( - "code.google.com/p/go.crypto/scrypt" - "crypto/sha1" - "crypto/aes" - "crypto/rsa" - "crypto/rand" - "crypto/cipher" - mrand "math/rand" - "math/big" - "io/ioutil" - "encoding/json" "bytes" + "code.google.com/p/go.crypto/scrypt" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" "encoding/binary" + "encoding/json" "errors" + "io/ioutil" + "math/big" + mrand "math/rand" + "os" "redoctober/padding" ) -// Constants for record type. +// Constants for record type const ( AESRecord = "AES" RSARecord = "RSA" ECCRecord = "ECC" ) -// Constants for scrypt. +// Constants for scrypt const ( - KEYLENGTH = 16 - N = 16384 - R = 8 - P = 1 + KEYLENGTH = 16 // 16-byte output from scrypt + N = 16384 // Cost parameter + R = 8 // Block size + P = 1 // Parallelization factor DEFAULT_VERSION = 1 ) - -// Set of encrypted records from disk -var records diskRecords // Path of current vault var localPath string -// DiskPasswordRecord is the set of password records on disk. -type DiskPasswordRecord struct { - Type string - Salt []byte +// PasswordRecord is the structure used to store password and key +// material for a single user name. It is written and read from +// storage in JSON format. +type PasswordRecord struct { + Type string + PasswordSalt []byte HashedPassword []byte - KeySalt []byte - AESKey []byte - RSAKey struct { - RSAExp []byte - RSAExpIV []byte - RSAPrimeP []byte + KeySalt []byte + AESKey []byte + RSAKey struct { + RSAExp []byte + RSAExpIV []byte + RSAPrimeP []byte RSAPrimePIV []byte - RSAPrimeQ []byte + RSAPrimeQ []byte RSAPrimeQIV []byte - RSAPublic rsa.PublicKey + RSAPublic rsa.PublicKey } Admin bool } + +// diskRecords is the structure used to read and write a JSON file +// containing the contents of a password vault type diskRecords struct { - Version int - VaultId int - HmacKey []byte - Passwords map[string]DiskPasswordRecord + Version int + VaultId int + HmacKey []byte + Passwords map[string]PasswordRecord } -// Summary is a minmal account summary. +// records is the set of encrypted records read from disk and +// unmarshalled +var records diskRecords + +// Summary is a minmial account summary. type Summary struct { Admin bool - Type string + Type string } -// Intialization. func init() { // seed math.random from crypto.random seedBytes, _ := makeRandom(8) seedBuf := bytes.NewBuffer(seedBytes) n64, _ := binary.ReadVarint(seedBuf) mrand.Seed(n64) - } -// Take a password and derive a scrypt hashed version -func hashPassword(password string, salt []byte) (hashPass []byte, err error) { +// hashPassword takes a password and derives a scrypt salted and hashed +// version +func hashPassword(password string, salt []byte) ([]byte, error) { return scrypt.Key([]byte(password), salt, N, R, P, KEYLENGTH) } -// Helper to make new buffer full of random data -func makeRandom(length int) (bytes []byte, err error) { - bytes = make([]byte, 16) - n, err := rand.Read(bytes) - if n != len(bytes) || err != nil { - return - } - return +// makeRandom is a helper that makes a new buffer full of random data +func makeRandom(length int) ([]byte, error) { + bytes := make([]byte, length) + _, err := rand.Read(bytes) + return bytes, err } -func encryptRSARecord(newRec *DiskPasswordRecord, rsaPriv *rsa.PrivateKey, passKey []byte) (err error) { - newRec.RSAKey.RSAExpIV, err = makeRandom(16) - if err != nil { +func encryptRSARecord(newRec *PasswordRecord, rsaPriv *rsa.PrivateKey, passKey []byte) (err error) { + if newRec.RSAKey.RSAExpIV, err = makeRandom(16); err != nil { return } - paddedExponent := padding.PadClearFile(rsaPriv.D.Bytes()) - newRec.RSAKey.RSAExp, err = encryptCBC(paddedExponent, newRec.RSAKey.RSAExpIV, passKey) - if err != nil { + paddedExponent := padding.AddPadding(rsaPriv.D.Bytes()) + if newRec.RSAKey.RSAExp, err = encryptCBC(paddedExponent, newRec.RSAKey.RSAExpIV, passKey); err != nil { return } - newRec.RSAKey.RSAPrimePIV, err = makeRandom(16) - if err != nil { + if newRec.RSAKey.RSAPrimePIV, err = makeRandom(16); err != nil { return } - paddedPrimeP := padding.PadClearFile(rsaPriv.Primes[0].Bytes()) - newRec.RSAKey.RSAPrimeP, err = encryptCBC(paddedPrimeP, newRec.RSAKey.RSAPrimePIV, passKey) - if err != nil { + paddedPrimeP := padding.AddPadding(rsaPriv.Primes[0].Bytes()) + if newRec.RSAKey.RSAPrimeP, err = encryptCBC(paddedPrimeP, newRec.RSAKey.RSAPrimePIV, passKey); err != nil { return } - newRec.RSAKey.RSAPrimeQIV, err = makeRandom(16) - if err != nil { + if newRec.RSAKey.RSAPrimeQIV, err = makeRandom(16); err != nil { return } - paddedPrimeQ := padding.PadClearFile(rsaPriv.Primes[1].Bytes()) + paddedPrimeQ := padding.AddPadding(rsaPriv.Primes[1].Bytes()) newRec.RSAKey.RSAPrimeQ, err = encryptCBC(paddedPrimeQ, newRec.RSAKey.RSAPrimeQIV, passKey) - if err != nil { - return - } return } // Create new record from username and password -func createPasswordRec(password string, admin bool) (newRec DiskPasswordRecord, err error) { +func createPasswordRec(password string, admin bool) (newRec PasswordRecord, err error) { newRec.Type = RSARecord - newRec.Salt, err = makeRandom(16) - if err != nil { + if newRec.PasswordSalt, err = makeRandom(16); err != nil { return } - newRec.HashedPassword, err = hashPassword(password, newRec.Salt) - if err != nil { + if newRec.HashedPassword, err = hashPassword(password, newRec.PasswordSalt); err != nil { return } - newRec.KeySalt, err = makeRandom(16) - if err != nil { + if newRec.KeySalt, err = makeRandom(16); err != nil { return } @@ -164,8 +161,7 @@ func createPasswordRec(password string, admin bool) (newRec DiskPasswordRecord, } // encrypt RSA key with password key - err = encryptRSARecord(&newRec, rsaPriv, passKey) - if err != nil { + if err = encryptRSARecord(&newRec, rsaPriv, passKey); err != nil { return } @@ -177,8 +173,7 @@ func createPasswordRec(password string, admin bool) (newRec DiskPasswordRecord, return } - newRec.AESKey, err = encryptECB(aesKey, passKey) - if err != nil { + if newRec.AESKey, err = encryptECB(aesKey, passKey); err != nil { return } @@ -187,13 +182,15 @@ func createPasswordRec(password string, admin bool) (newRec DiskPasswordRecord, return } -func derivePasswordKey(password string, keySalt []byte) (passwordKey []byte, err error) { +// derivePasswordKey generates a key from a password (and salt) using +// scrypt +func derivePasswordKey(password string, keySalt []byte) ([]byte, error) { return scrypt.Key([]byte(password), keySalt, N, R, P, KEYLENGTH) } -// Decrypt bytes using a key in ECB mode. -func decryptECB(data []byte, passwordKey []byte) (decryptedData []byte, err error) { - aesCrypt, err := aes.NewCipher(passwordKey) +// decryptECB decrypts bytes using a key in AES ECB mode. +func decryptECB(data, key []byte) (decryptedData []byte, err error) { + aesCrypt, err := aes.NewCipher(key) if err != nil { return } @@ -204,9 +201,9 @@ func decryptECB(data []byte, passwordKey []byte) (decryptedData []byte, err erro return } -// Decrypt bytes using a key in ECB mode. -func encryptECB(data []byte, passwordKey []byte) (encryptedData []byte, err error) { - aesCrypt, err := aes.NewCipher(passwordKey) +// encryptECB encrypts bytes using a key in AES ECB mode. +func encryptECB(data, key []byte) (encryptedData []byte, err error) { + aesCrypt, err := aes.NewCipher(key) if err != nil { return } @@ -217,9 +214,9 @@ func encryptECB(data []byte, passwordKey []byte) (encryptedData []byte, err erro return } -// Decrypt using a key and IV. -func decryptCBC(data []byte, iv []byte, passwordKey []byte) (decryptedData []byte, err error) { - aesCrypt, err := aes.NewCipher(passwordKey) +// decryptCBC decrypt bytes using a key and IV with AES in CBC mode. +func decryptCBC(data, iv, key []byte) (decryptedData []byte, err error) { + aesCrypt, err := aes.NewCipher(key) if err != nil { return } @@ -232,9 +229,9 @@ func decryptCBC(data []byte, iv []byte, passwordKey []byte) (decryptedData []byt return } -// Encrypt using a key and IV. -func encryptCBC(data []byte, iv []byte, passwordKey []byte) (encryptedData []byte, err error) { - aesCrypt, err := aes.NewCipher(passwordKey) +// encryptCBC encrypt data using a key and IV with AES in CBC mode. +func encryptCBC(data, iv, key []byte) (encryptedData []byte, err error) { + aesCrypt, err := aes.NewCipher(key) if err != nil { return } @@ -248,124 +245,127 @@ func encryptCBC(data []byte, iv []byte, passwordKey []byte) (encryptedData []byt } // InitFromDisk reads the record from disk and initialize global context. -func InitFromDisk(path string) { +func InitFromDisk(path string) error { jsonDiskRecord, err := ioutil.ReadFile(path) - if err == nil { - err = json.Unmarshal(jsonDiskRecord, &records) + + // It's OK for the file to be missing, we'll create it later if + // anything is added. + + if err != nil && !os.IsNotExist(err) { + return err } - // validate sizes - formatErr := false + // Initialized so that we can determine later if anything was read + // from the file. + + records.Version = 0 + + if len(jsonDiskRecord) != 0 { + if err = json.Unmarshal(jsonDiskRecord, &records); err != nil { + return err + } + } + + formatErr := errors.New("Format error") for _, rec := range records.Passwords { - if len(rec.Salt) != 16 { - formatErr = true + if len(rec.PasswordSalt) != 16 { + return formatErr } if len(rec.HashedPassword) != 16 { - formatErr = true + return formatErr } if len(rec.KeySalt) != 16 { - formatErr = true + return formatErr } if rec.Type == AESRecord { if len(rec.AESKey) != 16 { - formatErr = true + return formatErr } } if rec.Type == RSARecord { - if len(rec.RSAKey.RSAExp) == 0 || len(rec.RSAKey.RSAExp) % 16 != 0 { - formatErr = true + if len(rec.RSAKey.RSAExp) == 0 || len(rec.RSAKey.RSAExp)%16 != 0 { + return formatErr } - if len(rec.RSAKey.RSAPrimeP) == 0 || len(rec.RSAKey.RSAPrimeP) % 16 != 0 { - formatErr = true + if len(rec.RSAKey.RSAPrimeP) == 0 || len(rec.RSAKey.RSAPrimeP)%16 != 0 { + return formatErr } - if len(rec.RSAKey.RSAPrimeQ) == 0 || len(rec.RSAKey.RSAPrimeQ) % 16 != 0 { - formatErr = true + if len(rec.RSAKey.RSAPrimeQ) == 0 || len(rec.RSAKey.RSAPrimeQ)%16 != 0 { + return formatErr } - if len(rec.RSAKey.RSAExpIV) != 16 { - formatErr = true + if len(rec.RSAKey.RSAExpIV) != 16 { + return formatErr } - if len(rec.RSAKey.RSAPrimePIV) != 16 { - formatErr = true + if len(rec.RSAKey.RSAPrimePIV) != 16 { + return formatErr } - if len(rec.RSAKey.RSAPrimeQIV) != 16 { - formatErr = true + if len(rec.RSAKey.RSAPrimeQIV) != 16 { + return formatErr } } - if formatErr { - err = errors.New("Format error") - break - } } - if err != nil { + // If the Version field is 0 then it indicates that nothing was + // read from the file and so it needs to be initialized. + + if records.Version == 0 { records.Version = DEFAULT_VERSION records.VaultId = mrand.Int() records.HmacKey, err = makeRandom(16) if err != nil { - return + return err } - // make the record data holder - records.Passwords = make(map[string]DiskPasswordRecord) + records.Passwords = make(map[string]PasswordRecord) } localPath = path + + return nil } // WriteRecordsToDisk saves the current state of the records to disk. -func WriteRecordsToDisk() (err error) { +func WriteRecordsToDisk() error { if !IsInitialized() { - err = errors.New("Path not initialized") - return + return errors.New("Path not initialized") } - jsonDiskRecord, err := json.Marshal(records) - if err == nil { - err = ioutil.WriteFile(localPath, jsonDiskRecord, 0644) + if jsonDiskRecord, err := json.Marshal(records); err == nil { + return ioutil.WriteFile(localPath, jsonDiskRecord, 0644) + } else { + return err } - - return } // AddNewRecord adds a new record for a given username and password. -func AddNewRecord(name string, password string, admin bool) (passwordRec DiskPasswordRecord, err error) { - passwordRec, err = createPasswordRec(password, admin) - if err != nil { - return +func AddNewRecord(name, password string, admin bool) (PasswordRecord, error) { + if pr, err := createPasswordRec(password, admin); err == nil { + SetRecord(pr, name) + return pr, WriteRecordsToDisk() + } else { + return pr, err } - - SetRecord(passwordRec, name) - - err = WriteRecordsToDisk() - if err != nil { - return - } - - return } // ChangePassword changes the password for a given user. -func ChangePassword(name string, password string, newPassword string) (err error) { - // find and validate name and password - passwordRec, ok := GetRecord(name) +func ChangePassword(name, password, newPassword string) (err error) { + pr, ok := GetRecord(name) if !ok { err = errors.New("Record not present") return } - err = passwordRec.ValidatePassword(password) - if err != nil { + if err = pr.ValidatePassword(password); err != nil { return } // decrypt key var key []byte var rsaKey rsa.PrivateKey - if passwordRec.Type == AESRecord { - key, err = passwordRec.GetKeyAES(password) + if pr.Type == AESRecord { + key, err = pr.GetKeyAES(password) if err != nil { return } - } else if passwordRec.Type == RSARecord { - rsaKey, err = passwordRec.GetKeyRSA(password) + } else if pr.Type == RSARecord { + rsaKey, err = pr.GetKeyRSA(password) if err != nil { return } @@ -374,38 +374,30 @@ func ChangePassword(name string, password string, newPassword string) (err error return } - // create new salt - passwordRec.Salt, err = makeRandom(16) - if err != nil { + if pr.PasswordSalt, err = makeRandom(16); err != nil { + return + } + if pr.HashedPassword, err = hashPassword(newPassword, pr.PasswordSalt); err != nil { return } - // hash new password - passwordRec.HashedPassword, err = hashPassword(newPassword, passwordRec.Salt) - if err != nil { + if pr.KeySalt, err = makeRandom(16); err != nil { return } - - // create new key salt - passwordRec.KeySalt, err = makeRandom(16) - if err != nil { - return - } - - newPassKey, err := derivePasswordKey(newPassword, passwordRec.KeySalt) + newPassKey, err := derivePasswordKey(newPassword, pr.KeySalt) if err != nil { return } // encrypt original key with new password - if passwordRec.Type == AESRecord { - passwordRec.AESKey, err = encryptECB(key, newPassKey) + if pr.Type == AESRecord { + pr.AESKey, err = encryptECB(key, newPassKey) if err != nil { return } - } else if passwordRec.Type == RSARecord { - // encrypt RSA key with password key - err = encryptRSARecord(&passwordRec, &rsaKey, newPassKey) + } else if pr.Type == RSARecord { + // encrypt RSA key with password key + err = encryptRSARecord(&pr, &rsaKey, newPassKey) if err != nil { return } @@ -414,60 +406,52 @@ func ChangePassword(name string, password string, newPassword string) (err error return } - SetRecord(passwordRec, name) + SetRecord(pr, name) - // update disk record - err = WriteRecordsToDisk() - if err != nil { - return - } - - return + return WriteRecordsToDisk() } // DeleteRecord deletes a given record. func DeleteRecord(name string) error { if _, ok := GetRecord(name); ok { delete(records.Passwords, name) - } else { - return errors.New("Record missing") + return nil } - return nil + + return errors.New("Record missing") } // RevokeRecord removes admin status from a record. func RevokeRecord(name string) error { - rec, ok := GetRecord(name) - if ok { + if rec, ok := GetRecord(name); ok { rec.Admin = false SetRecord(rec, name) - } else { - return errors.New("Record missing") + return nil } - return nil + + return errors.New("Record missing") } // MakeAdmin adds admin status to a given record. func MakeAdmin(name string) error { - rec, ok := GetRecord(name) - if ok { + if rec, ok := GetRecord(name); ok { rec.Admin = true SetRecord(rec, name) - } else { - return errors.New("Record missing") + return nil } - return nil + + return errors.New("Record missing") } // SetRecord puts a record into the global status. -func SetRecord(passwordRec DiskPasswordRecord, name string) { - records.Passwords[name] = passwordRec +func SetRecord(pr PasswordRecord, name string) { + records.Passwords[name] = pr } // GetRecord returns a record given a name. -func GetRecord(name string) (passwordRec DiskPasswordRecord, ok bool) { - passwordRec, ok = records.Passwords[name] - return +func GetRecord(name string) (PasswordRecord, bool) { + dpr, found := records.Passwords[name] + return dpr, found } // GetVaultId returns the id of the current vault. @@ -502,71 +486,70 @@ func NumRecords() int { func GetSummary() (summary map[string]Summary) { summary = make(map[string]Summary) for name, pass := range records.Passwords { - tempName := Summary{pass.Admin, pass.Type} - summary[name] = tempName + summary[name] = Summary{pass.Admin, pass.Type} } return } -// IsAdmin returns the admin status of the DiskPasswordRecord. -func (passwordRec DiskPasswordRecord) IsAdmin() bool { - return passwordRec.Admin +// IsAdmin returns the admin status of the PasswordRecord. +func (pr PasswordRecord) IsAdmin() bool { + return pr.Admin } -// GetType returns the type status of the DiskPasswordRecord. -func (passwordRec DiskPasswordRecord) GetType() string { - return passwordRec.Type +// GetType returns the type status of the PasswordRecord. +func (pr PasswordRecord) GetType() string { + return pr.Type } // EncryptKey encrypts a 16-byte key with the RSA key of the record. -func (passwordRec DiskPasswordRecord) EncryptKey(in []byte) (out []byte, err error) { - return rsa.EncryptOAEP(sha1.New(), rand.Reader, &passwordRec.RSAKey.RSAPublic, in, nil) +func (pr PasswordRecord) EncryptKey(in []byte) (out []byte, err error) { + return rsa.EncryptOAEP(sha1.New(), rand.Reader, &pr.RSAKey.RSAPublic, in, nil) } // GetKeyAES returns the 16-byte key of the record. -func (passwordRec DiskPasswordRecord) GetKeyAES(password string) (key []byte, err error) { - if passwordRec.Type != AESRecord { +func (pr PasswordRecord) GetKeyAES(password string) (key []byte, err error) { + if pr.Type != AESRecord { return nil, errors.New("Invalid function for record type") } - err = passwordRec.ValidatePassword(password) + err = pr.ValidatePassword(password) if err != nil { return } - passKey, err := derivePasswordKey(password, passwordRec.KeySalt) + passKey, err := derivePasswordKey(password, pr.KeySalt) if err != nil { return } - return decryptECB(passwordRec.AESKey, passKey) + return decryptECB(pr.AESKey, passKey) } // GetKeyAES returns the RSA public key of the record. -func (passwordRec DiskPasswordRecord) GetKeyRSAPub() (out *rsa.PublicKey, err error) { - if passwordRec.Type != RSARecord { +func (pr PasswordRecord) GetKeyRSAPub() (out *rsa.PublicKey, err error) { + if pr.Type != RSARecord { return out, errors.New("Invalid function for record type") } - return &passwordRec.RSAKey.RSAPublic, err + return &pr.RSAKey.RSAPublic, err } // GetKeyAES returns the RSA private key of the record given the correct password. -func (passwordRec DiskPasswordRecord) GetKeyRSA(password string) (key rsa.PrivateKey, err error) { - if passwordRec.Type != RSARecord { +func (pr PasswordRecord) GetKeyRSA(password string) (key rsa.PrivateKey, err error) { + if pr.Type != RSARecord { return key, errors.New("Invalid function for record type") } - err = passwordRec.ValidatePassword(password) + err = pr.ValidatePassword(password) if err != nil { return } - passKey, err := derivePasswordKey(password, passwordRec.KeySalt) + passKey, err := derivePasswordKey(password, pr.KeySalt) if err != nil { return } - rsaExponentPadded, err := decryptCBC(passwordRec.RSAKey.RSAExp, passwordRec.RSAKey.RSAExpIV, passKey) + rsaExponentPadded, err := decryptCBC(pr.RSAKey.RSAExp, pr.RSAKey.RSAExpIV, passKey) if err != nil { return } @@ -575,7 +558,7 @@ func (passwordRec DiskPasswordRecord) GetKeyRSA(password string) (key rsa.Privat return } - rsaPrimePPadded, err := decryptCBC(passwordRec.RSAKey.RSAPrimeP, passwordRec.RSAKey.RSAPrimePIV, passKey) + rsaPrimePPadded, err := decryptCBC(pr.RSAKey.RSAPrimeP, pr.RSAKey.RSAPrimePIV, passKey) if err != nil { return } @@ -584,7 +567,7 @@ func (passwordRec DiskPasswordRecord) GetKeyRSA(password string) (key rsa.Privat return } - rsaPrimeQPadded, err := decryptCBC(passwordRec.RSAKey.RSAPrimeQ, passwordRec.RSAKey.RSAPrimeQIV, passKey) + rsaPrimeQPadded, err := decryptCBC(pr.RSAKey.RSAPrimeQ, pr.RSAKey.RSAPrimeQIV, passKey) if err != nil { return } @@ -593,7 +576,7 @@ func (passwordRec DiskPasswordRecord) GetKeyRSA(password string) (key rsa.Privat return } - key.PublicKey = passwordRec.RSAKey.RSAPublic + key.PublicKey = pr.RSAKey.RSAPublic key.D = big.NewInt(0).SetBytes(rsaExponent) key.Primes = []*big.Int{big.NewInt(0), big.NewInt(0)} key.Primes[0].SetBytes(rsaPrimeP) @@ -608,15 +591,14 @@ func (passwordRec DiskPasswordRecord) GetKeyRSA(password string) (key rsa.Privat } // ValidatePassword returns an error if the password is incorrect. -func (passwordRec DiskPasswordRecord) ValidatePassword(password string) (err error) { - sha, err := hashPassword(password, passwordRec.Salt) - if err != nil { - return +func (pr PasswordRecord) ValidatePassword(password string) error { + if h, err := hashPassword(password, pr.PasswordSalt); err != nil { + return err + } else { + if bytes.Compare(h, pr.HashedPassword) != 0 { + return errors.New("Wrong Password") + } } - if bytes.Compare(sha, passwordRec.HashedPassword) != 0 { - return errors.New("Wrong Password") - } - return + return nil } - diff --git a/src/redoctober/passvault/passvault_test.go b/src/redoctober/passvault/passvault_test.go index 4a209cc..8ddd961 100644 --- a/src/redoctober/passvault/passvault_test.go +++ b/src/redoctober/passvault/passvault_test.go @@ -1,3 +1,7 @@ +// passvault_test: tests for passvault.go +// +// Copyright (c) 2013 CloudFlare, Inc. + package passvault import ( diff --git a/src/redoctober/redoctober.go b/src/redoctober/redoctober.go index 6843511..6f7d860 100644 --- a/src/redoctober/redoctober.go +++ b/src/redoctober/redoctober.go @@ -1,84 +1,76 @@ // Package redoctober contains the server code for Red October. +// +// Copyright (c) 2013 CloudFlare, Inc. + package main import ( - "fmt" - "flag" - "os" - "io/ioutil" - "net" - "net/http" - "crypto/tls" "crypto/rand" + "crypto/tls" "crypto/x509" "encoding/pem" + "flag" + "fmt" + "io/ioutil" + "log" + "net" + "net/http" + "os" "redoctober/core" + "runtime" ) -// list of URLs to register -const ( - Create string = "/create" - Summary = "/summary" - Delegate = "/delegate" - Password = "/password" - Encrypt = "/encrypt" - Decrypt = "/decrypt" - Modify = "/modify" -) +// List of URLs to register and their related functions -// the channel handling user request -var process = make(chan userRequest) +var functions = map[string]func([]byte) ([]byte, error){ + "/create": core.Create, + "/summary": core.Summary, + "/delegate": core.Delegate, + "/password": core.Password, + "/encrypt": core.Encrypt, + "/decrypt": core.Decrypt, + "/modify": core.Modify, +} type userRequest struct { - rt string - in []byte - resp chan []byte + rt string // The request type (which will be one of the + // keys of the functions map above + in []byte // Arbitrary input data (depends on the core.* + // function called) + resp chan []byte // Channel down which a response is sent (the + // data sent will depend on the core.* function + // called to handle this request) } -func init() { - go func () { - for { - foo := <-process - switch { - case foo.rt == Create: - foo.resp <- core.Create(foo.in) - case foo.rt == Summary: - foo.resp <- core.Summary(foo.in) - case foo.rt == Delegate: - foo.resp <- core.Delegate(foo.in) - case foo.rt == Password: - foo.resp <- core.Password(foo.in) - case foo.rt == Encrypt: - foo.resp <- core.Encrypt(foo.in) - case foo.rt == Decrypt: - foo.resp <- core.Decrypt(foo.in) - case foo.rt == Modify: - foo.resp <- core.Modify(foo.in) - default: - fmt.Printf("Unknown! %s\n", foo.rt) - foo.resp <- []byte("Unknown command") - } - } - } () -} - -func queueRequest(requestType string, w http.ResponseWriter, r *http.Request, c *tls.ConnectionState) { +// queueRequest handles a single request receive on the JSON API for +// one of the functions named in the functions map above. It reads the +// request and sends it to the goroutine started in main() below for +// processing and then waits for the response. +func queueRequest(process chan userRequest, requestType string, w http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(r.Body) if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) return } - response := make(chan []byte, 1) - req := userRequest{rt: requestType, in: body, resp: response} - process <-req + response := make(chan []byte) + process <- userRequest{rt: requestType, in: body, resp: response} - code := <-response - - w.Write(code) + if resp, ok := <-response; ok { + w.Write(resp) + } else { + http.Error(w, "Unknown request", http.StatusInternalServerError) + } } -func NewServer(addr string, certPath string, keyPath string, caPath string) (*http.Server, *net.Listener, error) { - // set up server +// NewServer starts an HTTPS server the handles the redoctober JSON +// API. Each of the URIs in the functions map above is setup with a +// separate HandleFunc. Each HandleFunc is an instance of queueRequest +// above. +// +// Returns a valid http.Server handling redoctober JSON requests (and +// its associated listener) or an error +func NewServer(process chan userRequest, addr string, certPath, keyPath, caPath string) (*http.Server, *net.Listener, error) { mux := http.NewServeMux() srv := http.Server{ Addr: addr, @@ -86,35 +78,33 @@ func NewServer(addr string, certPath string, keyPath string, caPath string) (*ht } cert, err := tls.LoadX509KeyPair(certPath, keyPath) if err != nil { - fmt.Println(err) - return nil, nil, err + return nil, nil, fmt.Errorf("Error loading certificate (%s, %s): %s", certPath, keyPath, err) } config := tls.Config{ - Certificates: []tls.Certificate{cert}, - Rand: rand.Reader, - ClientAuth: tls.RequestClientCert, + Certificates: []tls.Certificate{cert}, + Rand: rand.Reader, + ClientAuth: tls.RequestClientCert, PreferServerCipherSuites: true, - SessionTicketsDisabled: true, + SessionTicketsDisabled: true, } - config.Rand = rand.Reader - // create local cert pool if present + // If a caPath has been specified then a local CA is being used + // and not the system configuration. + if caPath != "" { rootPool := x509.NewCertPool() pemCert, err := ioutil.ReadFile(caPath) if err != nil { - fmt.Println(err) - return nil, nil, err + return nil, nil, fmt.Errorf("Error reading %s: %s\n", caPath, err) } derCert, pemCert := pem.Decode(pemCert) if derCert == nil { - return nil, nil, err + return nil, nil, fmt.Errorf("Error decoding CA certificate: %s\n", err) } cert, err := x509.ParseCertificate(derCert.Bytes) if err != nil { - fmt.Println(err) - return nil, nil, err + return nil, nil, fmt.Errorf("Error parsing CA certificate: %s\n", err) } rootPool.AddCert(cert) @@ -123,16 +113,14 @@ func NewServer(addr string, certPath string, keyPath string, caPath string) (*ht conn, err := net.Listen("tcp", addr) if err != nil { - fmt.Println(err) - return nil, nil, err + return nil, nil, fmt.Errorf("Error starting TCP listener on %s: %s\n", addr, err) } lstnr := tls.NewListener(conn, &config) - for _, action := range []string {Create, Summary, Delegate, Password, Encrypt, Decrypt, Modify} { - var requestType = action + for requestType := range functions { mux.HandleFunc(requestType, func(w http.ResponseWriter, r *http.Request) { - queueRequest(requestType, w, r, r.TLS) + queueRequest(process, requestType, w, r) }) } @@ -144,11 +132,10 @@ const usage = `Usage: redoctober -vaultpath -addr -cert -key [-ca ] example: -redoctober /tmp/diskrecord.json localhost:8080 cert.pem cert.key - +redoctober -vaultpath /tmp/diskrecord.json -addr localhost:8080 -cert cert.pem -key cert.key ` -func main () { +func main() { flag.Usage = func() { fmt.Fprint(os.Stderr, usage) flag.PrintDefaults() @@ -168,8 +155,44 @@ func main () { os.Exit(2) } - core.Init(*vaultPath) - s, l, _ := NewServer(*addr, *certPath, *keyPath, *caPath) - s.Serve(*l) -} + if err := core.Init(*vaultPath); err != nil { + log.Fatalf(err.Error()) + } + runtime.GOMAXPROCS(runtime.NumCPU()) + + // The core package is not safe to be shared across goroutines so + // this supervisor goroutine reads requests from the process + // channel and dispatches them to core for processes. + + process := make(chan userRequest) + go func() { + for { + req := <-process + if f, ok := functions[req.rt]; ok { + r, err := f(req.in) + if err == nil { + req.resp <- r + } else { + log.Printf("Error handling %s: %s\n", req.rt, err) + } + } else { + log.Printf("Unknown user request received: %s\n", req.rt) + } + + // Note that if an error occurs no message is sent down + // the channel and then channel is closed. The + // queueRequest function will see this as indication of an + // error. + + close(req.resp) + } + }() + + s, l, err := NewServer(process, *addr, *certPath, *keyPath, *caPath) + if err == nil { + s.Serve(*l) + } else { + log.Fatalf("Error starting redoctober server: %s\n", err) + } +}