Add support for listing required delegations for an encrypted secret

This patch adds the /owners API endpoint that returns the list of users
that "own" the given secret. These are the users that can delegate their
passwords for decrypting the secret.

It also adds the "Get Owners" form in the web UI that uses the new API.

Fixes #62
This commit is contained in:
Alessandro Ghedini
2015-06-15 21:12:10 +02:00
parent 5328f286b9
commit 4183569465
5 changed files with 178 additions and 0 deletions

View File

@@ -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

View File

@@ -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})
}

View File

@@ -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\"}")

View File

@@ -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
}

View File

@@ -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(`<!DOCTYPE html>
<li><a href="#admin">Admin</a></li>
<li><a href="#encrypt">Encrypt</a></li>
<li><a href="#decrypt">Decrypt</a></li>
<li><a href="#owners">Owners</a></li>
</ul>
</div>
</div>
@@ -528,6 +530,22 @@ var indexHtml = []byte(`<!DOCTYPE html>
</form>
</div>
</section>
<hr />
<section class="row">
<div id="owners" class="col-md-6">
<h3>Get Owners</h3>
<form id="owners" class="ro-user-owners" role="form" action="/owners" method="post">
<div class="feedback owners-feedback"></div>
<div class="form-group">
<label for="owners-data">Data</label>
<textarea name="Data" class="form-control" id="owners-data" rows="5" required></textarea>
</div>
<button type="submit" class="btn btn-primary">Get Owners</button>
</form>
</div>
</section>
</div>
<footer id="footer" class="footer">
@@ -737,6 +755,20 @@ var indexHtml = []byte(`<!DOCTYPE html>
}
});
});
// Get owners
$('body').on('submit', 'form#owners', function(evt){
evt.preventDefault();
var $form = $(evt.currentTarget),
data = serialize($form);
submit( $form, {
data : data,
success : function(d){
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: '<p>Owners: '+d.Owners.sort().join(', ')+'</p>' }) );
}
});
});
});
</script>
</body>