Files
redoctober/redoctober.go
Joshua Kroll 638a25bbbc Add the ability to listen to systemd-provided sockets.
Add a new flag, -systemdfds, which causes Red October to expect to be
provisioned on launch with file descriptors for sockets opened by
systemd. This is useful for socket activation, but also allows systemd
to bind privileged ports for us. I've included example systemd
configuration files that successfully start Red October as a service
user without admin rights but bound to 443 in a Jessie VM for me. They
need to be installed where systemd expects them, which on Jessie is
/etc/systemd/system/redoctober.service and
/etc/systemd/system/sockets.target.wants/redoctober.socket.
2015-10-09 11:24:08 -07:00

794 lines
26 KiB
Go

// Package redoctober contains the server code for Red October.
//
// Copyright (c) 2013 CloudFlare, Inc.
package main
import (
"bytes"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"runtime"
"time"
"github.com/cloudflare/redoctober/core"
"github.com/coreos/go-systemd/activation"
)
// List of URLs to register and their related functions
var functions = map[string]func([]byte) ([]byte, error){
"/create": core.Create,
"/summary": core.Summary,
"/purge": core.Purge,
"/delegate": core.Delegate,
"/password": core.Password,
"/encrypt": core.Encrypt,
"/re-encrypt": core.ReEncrypt,
"/decrypt": core.Decrypt,
"/owners": core.Owners,
"/modify": core.Modify,
"/export": core.Export,
}
type userRequest struct {
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)
}
// 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)
process <- userRequest{rt: requestType, in: body, resp: response}
if resp, ok := <-response; ok {
header := w.Header()
header.Set("Content-Type", "application/json")
header.Set("Strict-Transport-Security", "max-age=86400; includeSubDomains; preload")
w.Write(resp)
} else {
http.Error(w, "Unknown request", http.StatusInternalServerError)
}
}
// 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, staticPath, addr, certPath, keyPath, caPath string, useSystemdSocket bool) (*http.Server, *net.Listener, error) {
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return nil, nil, fmt.Errorf("Error loading certificate (%s, %s): %s", certPath, keyPath, err)
}
config := tls.Config{
Certificates: []tls.Certificate{cert},
Rand: rand.Reader,
PreferServerCipherSuites: true,
SessionTicketsDisabled: true,
MinVersion: tls.VersionTLS10,
}
// If a caPath has been specified then a local CA is being used
// and not the system configuration.
if caPath != "" {
pemCert, err := ioutil.ReadFile(caPath)
if err != nil {
return nil, nil, fmt.Errorf("Error reading %s: %s\n", caPath, err)
}
derCert, _ := pem.Decode(pemCert)
if derCert == nil {
return nil, nil, fmt.Errorf("No PEM data was found in the CA certificate file\n")
}
cert, err := x509.ParseCertificate(derCert.Bytes)
if err != nil {
return nil, nil, fmt.Errorf("Error parsing CA certificate: %s\n", err)
}
rootPool := x509.NewCertPool()
rootPool.AddCert(cert)
config.ClientAuth = tls.RequireAndVerifyClientCert
config.ClientCAs = rootPool
}
var lstnr net.Listener
if useSystemdSocket {
listenFDs, err := activation.Listeners(true)
if err != nil {
log.Fatal(err)
}
if len(listenFDs) != 1 {
log.Fatal("Unexpected number of socket activation FDs!")
}
lstnr = listenFDs[0]
} else {
conn, err := net.Listen("tcp", addr)
if err != nil {
return nil, nil, fmt.Errorf("Error starting TCP listener on %s: %s\n", addr, err)
}
lstnr = tls.NewListener(conn, &config)
}
mux := http.NewServeMux()
// queue up post URIs
for current := range functions {
// copy this so reference does not get overwritten
requestType := current
mux.HandleFunc(requestType, func(w http.ResponseWriter, r *http.Request) {
log.Printf("http.server: endpoint=%s remote=%s", requestType, r.RemoteAddr)
queueRequest(process, requestType, w, r)
})
}
// queue up web frontend
idxHandler := &indexHandler{staticPath}
mux.HandleFunc("/index", idxHandler.handle)
mux.HandleFunc("/", idxHandler.handle)
srv := http.Server{
Addr: addr,
Handler: mux,
}
return &srv, &lstnr, nil
}
type indexHandler struct {
staticPath string
}
func (this *indexHandler) handle(w http.ResponseWriter, r *http.Request) {
var body io.ReadSeeker
if this.staticPath != "" {
f, err := os.Open(this.staticPath)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer f.Close()
body = f
} else {
body = bytes.NewReader(indexHtml)
}
header := w.Header()
header.Set("Content-Type", "text/html")
header.Set("Strict-Transport-Security", "max-age=86400; includeSubDomains; preload")
// If the server isn't HTTPS worthy, the HSTS header won't be honored.
http.ServeContent(w, r, "index.html", time.Now(), body)
}
const usage = `Usage:
redoctober -static <path> -vaultpath <path> -addr <addr> -cert <path> -key <path> [-ca <path>]
example:
redoctober -vaultpath diskrecord.json -addr localhost:8080 -cert cert.pem -key cert.key
`
func main() {
flag.Usage = func() {
fmt.Fprint(os.Stderr, usage)
flag.PrintDefaults()
os.Exit(2)
}
var staticPath = flag.String("static", "", "Path to override built-in index.html")
var vaultPath = flag.String("vaultpath", "diskrecord.json", "Path to the the disk vault")
var addr = flag.String("addr", "localhost:8080", "Server and port separated by :")
var useSystemdSocket = flag.Bool("systemdfds", false, "Use systemd socket activation to listen on a file. Useful for binding privileged sockets.")
var certPath = flag.String("cert", "", "Path of TLS certificate in PEM format")
var keyPath = flag.String("key", "", "Path of TLS private key in PEM format")
var caPath = flag.String("ca", "", "Path of TLS CA for client authentication (optional)")
flag.Parse()
if *vaultPath == "" || (*addr == "" && *useSystemdSocket == false) || *certPath == "" || *keyPath == "" {
fmt.Fprint(os.Stderr, usage)
flag.PrintDefaults()
os.Exit(2)
}
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("http.main failed: %s: %s", req.rt, err)
}
} else {
log.Printf("http.main: request=%s function is not supported", 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, *staticPath, *addr, *certPath, *keyPath, *caPath, *useSystemdSocket)
if err != nil {
log.Fatalf("Error starting redoctober server: %s\n", err)
}
s.Serve(*l)
}
var indexHtml = []byte(`<!DOCTYPE html>
<html lang="en">
<head>
<title>Red October - Two Man Rule File Encryption &amp; Decryption</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css" />
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap-theme.min.css" />
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.2/js/bootstrap.min.js"></script>
<style type="text/css">
.footer{ border-top: 1px solid #ccc; margin-top: 50px; padding: 20px 0;}
</style>
</head>
<body>
<nav class="navbar navbar-default" role="banner">
<div class="container">
<div class="navbar-header">
<a href="/" class="navbar-brand">Red October</a>
</div>
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li><a href="#delegate">Delegate</a></li>
<li><a href="#summary">Summary</a></li>
<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>
</nav>
<div class="container">
<h1 class="page-header">Red October Management</h1>
<section class="row">
<div id="delegate" class="col-md-6">
<h3>Delegate</h3>
<form id="user-delegate" class="ro-user-delegate" role="form" action="/delegate" method="post">
<div class="feedback delegate-feedback"></div>
<div class="form-group row">
<div class="col-md-6">
<label for="delegate-user">User name</label>
<input type="text" name="Name" class="form-control" id="delegate-user" placeholder="User name" required />
</div>
<div class="col-md-6">
<label for="delegate-user-pass">Password</label>
<input type="password" name="Password" class="form-control" id="delegate-user-pass" placeholder="Password" required />
</div>
</div>
<div class="form-group row">
<div class="col-md-6">
<label for="delegate-user-time">Delegation Time <small>(e.g., 2h34m)</small></label>
<input type="text" name="Time" class="form-control" id="delegate-user-time" placeholder="1h" required />
</div>
<div class="col-md-6">
<label for="delegate-uses">Uses</label>
<input type="number" name="Uses" class="form-control" id="delegate-uses" placeholder="5" required />
</div>
</div>
<div class="form-group row">
<div class="col-md-6">
<label for="delegate-users">Users to allow <small>(comma separated)</small></label>
<input type="text" name="Users" class="form-control" id="delegate-users" placeholder="e.g. Alice, Bob" />
</div>
<div class="col-md-6">
<label for="delegate-labels">Labels to allow <small>(comma separated)</small></label>
<input type="text" name="Labels" class="form-control" id="delegate-labels" placeholder="e.g. Blue, Red" />
</div>
</div>
<button type="submit" class="btn btn-primary">Delegate</button>
</form>
</div>
</section>
<hr />
<section class="row">
<div id="summary" class="col-md-6">
<h3>User summary / delegation list</h3>
<form id="vault-summary" class="form-inline ro-summary" role="form" action="/summary" method="post">
<div class="feedback summary-feedback"></div>
<div class="form-group">
<label class="sr-only" for="admin-user-auth">User name</label>
<input type="text" name="Name" class="form-control" id="admin-user-auth" placeholder="User name" required />
</div>
<div class="form-group">
<label class="sr-only" for="admin-pass-auth">Password</label>
<input type="password" name="Password" class="form-control" id="admin-pass-auth" placeholder="Password" required />
</div>
<button type="submit" class="btn btn-primary">Get Summary</button>
</form>
<div class="hide summary-results">
<h4>Current Delegations</h4>
<ul class="list-group summary-user-delegations"></ul>
<h4>All Users</h4>
<ul class="list-group summary-all-users"></ul>
</div>
</div>
</section>
<hr />
<section class="row">
<div class="col-md-6" id="admin">
<h3>Create vault</h3>
<form id="vault-create" class="form-inline ro-admin-create" role="form" action="/create" method="post">
<div class="feedback admin-feedback"></div>
<div class="form-group">
<label class="sr-only" for="admin-create-user">User name</label>
<input type="text" name="Name" class="form-control" id="admin-create-user" placeholder="User name" required />
</div>
<div class="form-group">
<label class="sr-only" for="admin-create-pass">Password</label>
<input type="password" name="Password" class="form-control" id="admin-create-pass" placeholder="Password" required />
</div>
<button type="submit" class="btn btn-primary">Create Admin</button>
</form>
<hr />
<h3>Create User</h3>
<form id="user-create" class="ro-user-create" role="form" action="/delegate" method="post">
<div class="feedback create-feedback"></div>
<div class="form-group row">
<div class="col-md-6">
<label for="create-user">User name</label>
<input type="text" name="Name" class="form-control" id="create-user" placeholder="User name" required />
</div>
<div class="col-md-6">
<label for="create-user-pass">Password</label>
<input type="password" name="Password" class="form-control" id="create-user-pass" placeholder="Password" required />
</div>
</div>
<div class="form-group row">
<div class="col-md-6">
<input type="hidden" name="Time" class="form-control" id="create-user-time" value="0h" required />
</div>
<div class="col-md-6">
<input type="hidden" name="Uses" class="form-control" id="create-uses" value="0" required />
</div>
</div>
<button type="submit" class="btn btn-primary">Create</button>
</form>
</div>
</section>
<hr />
<section class="row">
<div id="change-password" class="col-md-6">
<h3>Change password</h3>
<form id="user-change-password" class="ro-user-change-password" role="form" action="/password" method="post">
<div class="feedback change-password-feedback"></div>
<div class="form-group row">
<div class="col-md-6">
<label for="user-name">User name</label>
<input type="text" name="Name" class="form-control" id="user-name" placeholder="User name" required />
</div>
<div class="col-md-6">
<label for="user-pass">Password</label>
<input type="password" name="Password" class="form-control" id="user-pass" placeholder="Password" required />
</div>
</div>
<div class="form-group">
<label for="user-pass">New password</label>
<input type="password" name="NewPassword" class="form-control" id="user-pass-new" placeholder="New password" required />
</div>
<button type="submit" class="btn btn-primary">Change password</button>
</form>
<h3>Modify user</h3>
<form id="user-modify" class="ro-user-modify" role="form" action="/modify" method="post">
<div class="feedback modify-feedback"></div>
<div class="form-group row">
<div class="col-md-6">
<label for="modify-user-admin">Admin User</label>
<input type="text" name="Name" class="form-control" id="modify-user-admin" placeholder="User name" required />
</div>
<div class="col-md-6">
<label for="modify-user-pass">Admin Password</label>
<input type="password" name="Password" class="form-control" id="modify-user-pass" placeholder="Password" required />
</div>
</div>
<div class="form-group row">
<div class="col-md-6">
<label for="modify-user-user">User to modify <small>(e.g., Carol)</small></label>
<input type="text" name="ToModify" class="form-control" id="modify-user-user" required />
</div>
<div class="col-md-6">
<label for="modify-user-command">Command</label>
<select id="modify-user-command" name="Command" class="form-control" required>
<option value="revoke">Revoke Admin Status</option>
<option value="admin">Make Admin</option>
<option value="delete">Delete User</option>
</select>
</div>
</div>
<button type="submit" class="btn btn-primary">Modify user</button>
</form>
</div>
</section>
<hr />
<section class="row">
<div id="encrypt" class="col-md-6">
<h3>Encrypt data</h3>
<form id="encrypt" class="ro-user-encrypt" role="form" action="/encrypt" method="post">
<div class="feedback encrypt-feedback"></div>
<div class="form-group row">
<div class="col-md-6">
<label for="encrypt-user-admin">User name</label>
<input type="text" name="Name" class="form-control" id="encrypt-user-admin" placeholder="User name" required />
</div>
<div class="col-md-6">
<label for="encrypt-user-pass">Password</label>
<input type="password" name="Password" class="form-control" id="encrypt-user-pass" placeholder="Password" required />
</div>
</div>
<div class="form-group row">
<div class="col-md-6">
<label for="encrypt-minimum">Minimum number of users for access</label>
<input type="number" name="Minimum" class="form-control" id="encrypt-minimum" placeholder="2" required />
</div>
<div class="col-md-6">
<label for="encrypt-owners">Owners <small>(comma separated users)</small></label>
<input type="text" name="Owners" class="form-control" id="encrypt-owners" placeholder="e.g., Carol, Bob" required />
</div>
</div>
<div class="form-group row">
<div class="col-md-6">
<label for="encrypt-labels">Labels to use <small>(comma separated)</small></label>
<input type="text" name="Labels" class="form-control" id="encrypt-labels" placeholder="e.g. Blue, Red" />
</div>
</div>
<div class="form-group">
<label for="encrypt-data">Data <small>(not base64 encoded)</small></label>
<textarea name="Data" class="form-control" id="encrypt-data" rows="5" required></textarea>
</div>
<button type="submit" class="btn btn-primary">Encrypt!</button>
</form>
</div>
</section>
<hr />
<section class="row">
<div id="decrypt" class="col-md-6">
<h3>Decrypt data</h3>
<form id="decrypt" class="ro-user-decrypt" role="form" action="/decrypt" method="post">
<div class="feedback decrypt-feedback"></div>
<div class="form-group row">
<div class="col-md-6">
<label for="decrypt-user-admin">User name</label>
<input type="text" name="Name" class="form-control" id="decrypt-user-admin" placeholder="User name" required />
</div>
<div class="col-md-6">
<label for="decrypt-user-pass">Password</label>
<input type="password" name="Password" class="form-control" id="decrypt-user-pass" placeholder="Password" required />
</div>
</div>
<div class="form-group">
<label for="decrypt-data">Data</label>
<textarea name="Data" class="form-control" id="decrypt-data" rows="5" required></textarea>
</div>
<button type="submit" class="btn btn-primary">Decrypt!</button>
</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">
<p class="container">Red October. CloudFlare</p>
</footer>
<script>
$(function(){
function serialize( $form ){
var serialized = $form.serializeArray(), data = {};
$.each(serialized, function(idx, item){ data[item.name] = item.value; });
return data;
}
function makeAlert(config){ return '<div class="alert alert-dismissable alert-'+config.type+'"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>'+config.message+'</div>'; }
function submit( $form, options ){
options || (options = {});
$.ajax({
url: $form.attr('action'),
data: JSON.stringify( options.data ),
success: function(data){
if( data.Status !== 'ok' ){
$form.find('.feedback').empty().append( makeAlert({type: 'danger', message: data.Status}) );
return;
}
if( options.success ){
options.success.apply(this, arguments);
}
$form.get(0).reset();
},
error: options.error || function(xhr, status, error){ $form.find('.feedback').append( makeAlert({type:'danger', message: error})); }
});
}
// Ajax defaults for JSON
$.ajaxSetup({
method: 'POST',
dataType : 'json',
processData : false
});
// Create vault/admin
$('body').on('submit', '#vault-create', 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: 'Created user: '+data.Name }) );
}
});
});
// Vault summary
$('body').on('submit', '#vault-summary', function(evt){
evt.preventDefault();
var $form = $(evt.currentTarget),
data = serialize($form);
$('#summary .feedback').empty();
submit($form, {
data : data,
success : function(data){
// Empty out the lists
$('.summary-user-delegations, .summary-all-users').empty();
function buildItem(key, user, loc){
var li = $('<li />', {'class': 'list-group-item'}).appendTo(loc);
if( user.Uses ){ li.append( $('<span />', {'class': 'badge'}).text(user.Uses+' uses remaining') ); }
li.append( $('<h5 />', {'class': 'list-group-item-heading'}).text(key || 'Unknown') );
li.append( $('<p />', {'class': 'list-group-item-text'}).html('Type: '+user.Type+ (user.Expiry ? '<br />Expiry: '+user.Expiry : '')+ (user.Users ? '<br />Users: '+user.Users.join(', ') : '')+ (user.Labels ? '<br />Labels: '+user.Labels.join(', ') : '')) );
if( user.Admin ){
li.find('h5').append(' (admin)');
}
}
function buildLiveItem(k,u){ buildItem(k,u,'.summary-user-delegations'); }
function buildAllItem(k,u){ buildItem(k,u,'.summary-all-users'); }
$.each(data.Live, buildLiveItem);
$.each(data.All, buildAllItem);
$('.summary-results').removeClass('hide');
}
})
});
// Delegate
$('body').on('submit', '#user-delegate', function(evt){
evt.preventDefault();
var $form = $(evt.currentTarget),
data = serialize($form);
// Force uses to an integer
data.Uses = parseInt(data.Uses, 10);
data.Users = data.Users.split(',');
for(var i=0, l=data.Users.length; i<l; i++){
data.Users[i] = data.Users[i].trim();
if (data.Users[i] == "") { data.Users.splice(i, 1); }
}
data.Labels = data.Labels.split(',');
for(var i=0, l=data.Labels.length; i<l; i++){
data.Labels[i] = data.Labels[i].trim();
if (data.Labels[i] == "") { data.Labels.splice(i, 1); }
}
submit( $form, {
data : data,
success : function(d){
$form.find('.feedback').append( makeAlert({ type: 'success', message: 'Delegating '+data.Name }) );
}
});
});
// Create
$('body').on('submit', '#user-create', function(evt){
evt.preventDefault();
var $form = $(evt.currentTarget),
data = serialize($form);
// Force uses to an integer
data.Uses = parseInt(data.Uses, 10);
submit( $form, {
data : data,
success : function(d){
$form.find('.feedback').append( makeAlert({ type: 'success', message: 'Creating '+data.Name }) );
}
});
});
// Change password
$('body').on('submit', '#user-change-password', 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: 'Change password for '+data.Name }) );
}
});
});
// Modify user
$('body').on('submit', '#user-modify', 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: 'Successfully modified '+data.ToModify }) );
}
});
});
// Encrypt data
$('body').on('submit', '#encrypt', function(evt){
evt.preventDefault();
var $form = $(evt.currentTarget),
data = serialize($form);
data.Minimum = parseInt(data.Minimum, 10);
data.Owners = data.Owners.split(',');
for(var i=0, l=data.Owners.length; i<l; i++){
data.Owners[i] = data.Owners[i].trim();
if (data.Owners[i] == "") { data.Owners.splice(i, 1); }
}
data.Labels = data.Labels.split(',');
for(var i=0, l=data.Labels.length; i<l; i++){
data.Labels[i] = data.Labels[i].trim();
if (data.Labels[i] == "") { data.Labels.splice(i, 1); }
}
// Convert data to base64.
data.Data = window.btoa(data.Data);
submit( $form, {
data : data,
success : function(d){
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: '<p>Successfully encrypted data:</p><pre>'+d.Response+'</pre>' }) );
}
});
});
// Decrypt data
$('body').on('submit', 'form#decrypt', function(evt){
evt.preventDefault();
var $form = $(evt.currentTarget),
data = serialize($form);
submit( $form, {
data : data,
success : function(d){
d = JSON.parse(window.atob(d.Response));
$form.find('.feedback').empty().append( makeAlert({ type: (d.Secure ? 'success' : 'warning'), message: '<p>Successfully decrypted data:</p><pre>'+ window.atob(d.Data)+'</pre><p>Delegates: '+d.Delegates.sort().join(', ')+'</p>' }) );
}
});
});
// 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>
</html>`)