diff --git a/README.md b/README.md index da0e27d..783092b 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ format is POSTed and JSON is returned. - `/modify`: Modify permissions - `/encrypt`: Encrypt - `/decrypt`: Decrypt + - `/owners`: List owners of an encrypted secret. - `/summary`: Display summary of the delegates - `/password`: Change password - `/index`: Optionally, the server can host a static HTML file. @@ -178,6 +179,17 @@ If there aren't enough keys delegated you'll see: {"Status":"Need more delegated keys"} +### Owners + +Owners allows users to determine which delegations are needed to decrypt +a piece of data. + +Example query: + + $ curl --cacert cert/server.crt https://localhost:8080/owners \ + -d '{"Data":"eyJWZXJzaW9uIj...NSSllzPSJ9"}' + {"Status":"ok","Owners":["Alice","Bill","Cat","Dodo"]} + ### Password Password allows a user to change their password. This password change diff --git a/core/core.go b/core/core.go index f849ef9..49b7e88 100644 --- a/core/core.go +++ b/core/core.go @@ -73,6 +73,10 @@ type DecryptRequest struct { Data []byte } +type OwnersRequest struct { + Data []byte +} + type ModifyRequest struct { Name string Password string @@ -100,6 +104,11 @@ type DecryptWithDelegates struct { Delegates []string } +type OwnersData struct { + Status string + Owners []string +} + // Helper functions that create JSON responses sent by core func jsonStatusOk() ([]byte, error) { @@ -366,3 +375,21 @@ func Modify(jsonIn []byte) ([]byte, error) { return jsonStatusOk() } } + +// Owners processes a owners request. +func Owners(jsonIn []byte) ([]byte, error) { + var s OwnersRequest + err := json.Unmarshal(jsonIn, &s) + if err != nil { + log.Println("Error unmarshaling input:", err) + return jsonStatusError(err) + } + + names, err := crypt.GetOwners(s.Data) + if err != nil { + log.Println("Error listing owners:", err) + return jsonStatusError(err) + } + + return json.Marshal(OwnersData{Status: "ok", Owners: names}) +} diff --git a/core/core_test.go b/core/core_test.go index 688c8b6..fb5f7ec 100644 --- a/core/core_test.go +++ b/core/core_test.go @@ -7,7 +7,9 @@ package core import ( "bytes" "encoding/json" + "reflect" "os" + "sort" "testing" "github.com/cloudflare/redoctober/passvault" @@ -453,6 +455,59 @@ func TestEncryptDecrypt(t *testing.T) { } } +func TestOwners(t *testing.T) { + delegateJson := []byte("{\"Name\":\"Alice\",\"Password\":\"Hello\",\"Time\":\"0s\",\"Uses\":0}") + delegateJson2 := []byte("{\"Name\":\"Bob\",\"Password\":\"Hello\",\"Time\":\"0s\",\"Uses\":0}") + delegateJson3 := []byte("{\"Name\":\"Carol\",\"Password\":\"Hello\",\"Time\":\"0s\",\"Uses\":0}") + encryptJson := []byte("{\"Name\":\"Carol\",\"Password\":\"Hello\",\"Minumum\":2,\"Owners\":[\"Alice\",\"Bob\",\"Carol\"],\"Data\":\"SGVsbG8gSmVsbG8=\"}") + + var s ResponseData + var l OwnersData + + Init("memory") + + Create(delegateJson) + Delegate(delegateJson2) + Delegate(delegateJson3) + + // Encrypt with non-admin + respJson, err := Encrypt(encryptJson) + if err != nil { + t.Fatalf("Error in encrypt, %v", err) + } + err = json.Unmarshal(respJson, &s) + if err != nil { + t.Fatalf("Error in encrypt, %v", err) + } + if s.Status != "ok" { + t.Fatalf("Error in encrypt, %v", s.Status) + } + + // Get owners list + ownersJson, err := json.Marshal(OwnersRequest{Data: s.Response}) + if err != nil { + t.Fatalf("Error in owners, %v", err) + } + respJson, err = Owners(ownersJson) + if err != nil { + t.Fatalf("Error in owners, %v", err) + } + err = json.Unmarshal(respJson, &l) + if err != nil { + t.Fatalf("Error in owners, %v", err) + } + if l.Status != "ok" { + t.Fatalf("Error in owners, %v", l.Status) + } + + sort.Strings(l.Owners) + + expectedOwners := []string{"Alice", "Bob", "Carol"} + if !reflect.DeepEqual(l.Owners, expectedOwners) { + t.Fatalf("Owners list mismatch, %v", l.Owners) + } +} + func TestModify(t *testing.T) { summaryJson := []byte("{\"Name\":\"Alice\",\"Password\":\"Hello\"}") summaryJson2 := []byte("{\"Name\":\"Carol\",\"Password\":\"Hello\"}") diff --git a/cryptor/cryptor.go b/cryptor/cryptor.go index 7d92902..830b188 100644 --- a/cryptor/cryptor.go +++ b/cryptor/cryptor.go @@ -471,3 +471,55 @@ func (c *Cryptor) Decrypt(in []byte, user string) (resp []byte, names []string, resp, err = padding.RemovePadding(clearData) return } + +// GetOwners returns the list of users that can delegate their passwords +// to decrypt the given encrypted secret. +func (c *Cryptor) GetOwners(in []byte) (names []string, 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 { + err = errors.New("Unknown version") + return + } + + hmacKey, err := c.records.GetHMACKey() + if err != nil { + return + } + + if err = encrypted.unlock(hmacKey); err != nil { + return + } + + // make sure file was encrypted with the active vault + vaultId, err := c.records.GetVaultID() + if err != nil { + return + } + if encrypted.VaultId != vaultId { + err = errors.New("Wrong vault") + return + } + + // compute HMAC + expectedMAC := encrypted.computeHmac(hmacKey) + if !hmac.Equal(encrypted.Signature, expectedMAC) { + err = errors.New("Signature mismatch") + return + } + + addedNames := make(map[string]bool) + for _, mwKey := range encrypted.KeySet { + for _, mwName := range mwKey.Name { + if !addedNames[mwName] { + names = append(names, mwName) + addedNames[mwName] = true + } + } + } + + return +} diff --git a/redoctober.go b/redoctober.go index 10768b5..459d55e 100644 --- a/redoctober.go +++ b/redoctober.go @@ -33,6 +33,7 @@ var functions = map[string]func([]byte) ([]byte, error){ "/password": core.Password, "/encrypt": core.Encrypt, "/decrypt": core.Decrypt, + "/owners": core.Owners, "/modify": core.Modify, } @@ -273,6 +274,7 @@ var indexHtml = []byte(`