[blkctl] Add zxcrypt

This CL adds the zxcrypt function objects, allowing blkctl to create,
query, and destroy zxcrypt volumes.

Test: Unit tests are broken!

Change-Id: Ic2c37b8bad35a106911eef8d6eedbcdd7492f7cf
diff --git a/system/ulib/blkctl/blkctl.cpp b/system/ulib/blkctl/blkctl.cpp
index c62c8c2..aa5d067 100644
--- a/system/ulib/blkctl/blkctl.cpp
+++ b/system/ulib/blkctl/blkctl.cpp
@@ -2,7 +2,9 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include <ctype.h>
 #include <inttypes.h>
+#include <limits.h>
 #include <stddef.h>
 #include <stdint.h>
 #include <stdio.h>
@@ -19,11 +21,12 @@
 #include "fvm.h"
 #include "generic.h"
 #include "ramdisk.h"
+#include "zxcrypt.h"
 
 namespace blkctl {
 namespace {
 
-#define ADD_CMD_TYPE(T)                                                                         \
+#define ADD_CMD_TYPE(T)                                                                            \
     { T::kType, T::kCommands, T::kNumCommands }
 struct CmdType {
     const char* type;
@@ -42,6 +45,7 @@
 constexpr CmdType kTypes[] = {
     ADD_CMD_TYPE(ramdisk),
     ADD_CMD_TYPE(fvm),
+    ADD_CMD_TYPE(zxcrypt),
     // The generic commands should be last, so that various routines use them if no type matches
     ADD_CMD_TYPE(generic),
 };
@@ -103,12 +107,11 @@
         return rc;
     }
 
-    Command *cmd = tmp.cmd();
+    Command* cmd = tmp.cmd();
     return cmd->Run();
 }
 
-
-zx_status_t BlkCtl::Parse(int argc, char** argv) {
+zx_status_t BlkCtl::Parse(int argc, char** argv, const char* canned) {
     zx_status_t rc;
 
     if (argc == 0 || !argv) {
@@ -116,6 +119,11 @@
         return ZX_ERR_INVALID_ARGS;
     }
 
+    // Reset the internal state of the command line
+    force_ = false;
+    argn_ = 0;
+    canned_ = canned;
+
     // Consume binname
     fbl::AllocChecker ac;
     binname_.Set(argv[0], &ac);
@@ -240,7 +248,9 @@
     }
     printf("About to commit changes to disk.  Are you sure? [y/N] ");
     fflush(stdout);
-    switch (getchar()) {
+    int c = fgetc(stdin);
+    printf("\n");
+    switch (c) {
     case 'y':
     case 'Y':
         return ZX_OK;
@@ -249,4 +259,30 @@
     }
 }
 
+zx_status_t BlkCtl::Prompt(const char* prompt, char* s, size_t n) {
+    if (!canned_) {
+        printf("Enter %s: ", prompt);
+        fflush(stdout);
+    }
+    for (size_t i = 0; i < n;) {
+        int c = canned_ ? *canned_++ : fgetc(stdin);
+        if (c == 0x7f && i != 0) {
+            printf("\x1b[D\x1b[K");
+            fflush(stdout);
+            --i;
+        } else if (c == '\0' || c == '\n' || c == '\r') {
+            s[i] = '\0';
+            printf("\n");
+            break;
+        } else if (!iscntrl(c)) {
+            printf("%c", c);
+            fflush(stdout);
+            s[i] = static_cast<char>(c);
+            ++i;
+        }
+    }
+
+    return ZX_OK;
+}
+
 } // namespace blkctl
diff --git a/system/ulib/blkctl/command.cpp b/system/ulib/blkctl/command.cpp
index 93c71a6..9cc8932 100644
--- a/system/ulib/blkctl/command.cpp
+++ b/system/ulib/blkctl/command.cpp
@@ -23,7 +23,7 @@
 namespace {
 
 // Byte offsets where dashes are placed in a printed GUID
-constexpr size_t kGuidDashes[] = {4, 6, 8, 10, GUID_LEN};
+constexpr size_t kGuidSegmentLens[] = {4, 2, 2, 2, 6};
 
 } // namespace
 
@@ -41,28 +41,21 @@
     out[8] = (out[8] & 0x3F) | 0x80;
 }
 
-zx_status_t Command::ParseGuid(const char* guid, uint8_t* out, size_t out_len) {
-    ZX_DEBUG_ASSERT(out);
-    ZX_DEBUG_ASSERT(out_len == GUID_LEN);
-
+zx_status_t Command::ParseHex(const char* str, uint8_t* out, size_t out_len, char delim) {
+    ZX_DEBUG_ASSERT(out || out_len == 0);
     auto printError =
-        fbl::MakeAutoCall([&] { fprintf(stderr, "failed to parse GUID: %s\n", guid); });
-    const char* p = guid;
-    size_t i;
-    size_t j = 0;
-    for (i = 0; i < out_len; ++i) {
-        if (i == kGuidDashes[j]) {
-            if (*p++ != '-') {
-                return ZX_ERR_INVALID_ARGS;
-            }
-            ++j;
-        }
+        fbl::MakeAutoCall([str] { fprintf(stderr, "failed to parse hex: '%s'\n", str); });
+    if (strlen(str) < out_len * 2) {
+        return ZX_ERR_INVALID_ARGS;
+    }
+    const char* p = str;
+    for (size_t i = 0; i < out_len; ++i) {
         if (sscanf(p, "%02" SCNx8, &out[i]) != 1) {
             return ZX_ERR_INVALID_ARGS;
         }
         p += 2;
     }
-    if (*p) {
+    if (*p != delim) {
         return ZX_ERR_INVALID_ARGS;
     }
     printError.cancel();
@@ -70,15 +63,43 @@
     return ZX_OK;
 }
 
-void Command::PrintGuid(const uint8_t* guid, size_t guid_len) {
-    ZX_DEBUG_ASSERT(guid_len == GUID_LEN);
-    size_t j = 0;
-    for (size_t i = 0; i < guid_len; ++i) {
-        if (i == kGuidDashes[j]) {
-            printf("-");
-            ++j;
+zx_status_t Command::ParseGuid(const char* guid, uint8_t* out, size_t out_len) {
+    ZX_DEBUG_ASSERT(out);
+    ZX_DEBUG_ASSERT(out_len == GUID_LEN);
+    zx_status_t rc;
+
+    for (size_t i = 0; i < sizeof(kGuidSegmentLens) / sizeof(kGuidSegmentLens[0]); ++i) {
+        size_t segment_len = kGuidSegmentLens[i];
+        if (out_len < segment_len) {
+            return ZX_ERR_INVALID_ARGS;
         }
-        printf("%02" PRIx8, guid[i]);
+        if (out_len > segment_len) {
+            rc = ParseHex(guid, out, segment_len, '-');
+        } else {
+            rc = ParseHex(guid, out, out_len);
+        }
+        if (rc != ZX_OK) {
+            return rc;
+        }
+        guid += (2 * segment_len) + 1;
+        out += segment_len;
+        out_len -= segment_len;
+    }
+
+    return ZX_OK;
+}
+
+void Command::PrintGuid(const uint8_t* guid, size_t guid_len) {
+    for (size_t i = 0; i < sizeof(kGuidSegmentLens) / sizeof(kGuidSegmentLens[0]); ++i) {
+        size_t segment_len = kGuidSegmentLens[i];
+        ZX_DEBUG_ASSERT(guid_len >= segment_len);
+        for (size_t j = 0; j < segment_len; ++j) {
+            printf("%02" PRIx8, *guid++);
+        }
+        guid_len -= segment_len;
+        if (guid_len != 0) {
+            printf("-");
+        }
     }
 }
 
diff --git a/system/ulib/blkctl/fvm.cpp b/system/ulib/blkctl/fvm.cpp
index ef58ebd..a4d77ff 100644
--- a/system/ulib/blkctl/fvm.cpp
+++ b/system/ulib/blkctl/fvm.cpp
@@ -168,7 +168,6 @@
         (rc = cmdline->Confirm()) != ZX_OK || (rc = ReopenWritable(&fd)) != ZX_OK) {
         return rc;
     }
-
     GenerateGuid(request.guid, sizeof(request.guid));
     if ((res = ioctl_block_fvm_alloc_partition(fd, &request)) < 0) {
         rc = static_cast<zx_status_t>(res);
diff --git a/system/ulib/blkctl/fvm.h b/system/ulib/blkctl/fvm.h
index 2fc4aa6..7d3ae1d 100644
--- a/system/ulib/blkctl/fvm.h
+++ b/system/ulib/blkctl/fvm.h
@@ -21,18 +21,21 @@
 constexpr const char* kType = "fvm";
 
 constexpr Cmd kCommands[] = {
-    {"init", "<device> <slice_size>", "Format a block device to be an empty FVM volume.", Instantiate<Init>},
+    {"init", "<device> <slice_size>", "Format a block device to be an empty FVM volume.",
+     Instantiate<Init>},
     {"dump", "<device>", "Dump block device and FVM volume information.", Instantiate<Dump>},
-    {"add", "<device> <name> <slices> [type-guid]", "Allocates a new partition in the FVM volume.", Instantiate<Add>},
+    {"add", "<device> <name> <slices> [type-guid]", "Allocates a new partition in the FVM volume.",
+     Instantiate<Add>},
     {"query", "<device>", "List ranges of allocated slices.", Instantiate<Query>},
-    {"extend", "<device> <start> <length>", "Allocates slices for the partition.", Instantiate<Extend>},
+    {"extend", "<device> <start> <length>", "Allocates slices for the partition.",
+     Instantiate<Extend>},
     {"shrink", "<device> <start> <length>", "Free slices from the partition.", Instantiate<Shrink>},
     {"remove", "<device>", "Removes partition from the FVM volume.", Instantiate<Remove>},
     {"destroy", "<device>", "Overwrites and unbinds an FVM volume.", Instantiate<Destroy>},
 };
 constexpr size_t kNumCommands = sizeof(kCommands) / sizeof(kCommands[0]);
 
-constexpr const char *kDriver = "/boot/driver/fvm.so";
+constexpr const char* kDriver = "/boot/driver/fvm.so";
 
 } // namespace fvm
 } // namespace blkctl
diff --git a/system/ulib/blkctl/generic.cpp b/system/ulib/blkctl/generic.cpp
index f848979..848ec85 100644
--- a/system/ulib/blkctl/generic.cpp
+++ b/system/ulib/blkctl/generic.cpp
@@ -43,7 +43,7 @@
 
     DIR* dptr = opendir(kDevClassBlock);
     ZX_DEBUG_ASSERT(dptr);
-    auto cleanup = fbl::MakeAutoCall([&]{closedir(dptr);});
+    auto cleanup = fbl::MakeAutoCall([&] { closedir(dptr); });
 
     struct dirent* dent;
     printf("%8s    %s\n", "ID", "Topological path");
diff --git a/system/ulib/blkctl/generic.h b/system/ulib/blkctl/generic.h
index 5c75489..d8b7ec0 100644
--- a/system/ulib/blkctl/generic.h
+++ b/system/ulib/blkctl/generic.h
@@ -13,7 +13,7 @@
 DEFINE_COMMAND(List);
 DEFINE_COMMAND(Dump);
 
-constexpr const char *kType = nullptr;
+constexpr const char* kType = nullptr;
 
 constexpr Cmd kCommands[] = {
     {"help", "", "Print this message and exit.", Instantiate<generic::Help>},
diff --git a/system/ulib/blkctl/include/blkctl/blkctl.h b/system/ulib/blkctl/include/blkctl/blkctl.h
index 530d382..50298c5 100644
--- a/system/ulib/blkctl/include/blkctl/blkctl.h
+++ b/system/ulib/blkctl/include/blkctl/blkctl.h
@@ -7,6 +7,7 @@
 #include <stddef.h>
 #include <stdint.h>
 
+#include <blkctl/command.h>
 #include <fbl/macros.h>
 #include <fbl/string.h>
 #include <fbl/unique_ptr.h>
@@ -19,20 +20,23 @@
 
 class BlkCtl final {
 public:
-    BlkCtl()
-        : force_(false), argn_(0) {}
+    BlkCtl() : force_(false), argn_(0), canned_(nullptr) {}
     ~BlkCtl() {}
 
+    const Command* cmd() const { return cmd_ ? cmd_.get() : nullptr; }
     Command* cmd() { return cmd_ ? cmd_.get() : nullptr; }
 
+    void set_force(bool force) { force_ = force; }
+
     // Prints usage information based on the available |CommandSets|.
     void Usage() const;
 
-    // Converts the command line arguments into a |Command| object and runs it..
+    // Converts the command line arguments into a |Command| object and runs it.
     static zx_status_t Execute(int argc, char** argv);
 
-    // Converts the command line arguments into a |Command| object and runs it..
-    zx_status_t Parse(int argc, char** argv);
+    // Converts the command line arguments into a |Command| object and runs it.  Tests can provide
+    // |canned| responses for |Prompt|, delimited by '\n'.
+    zx_status_t Parse(int argc, char** argv, const char* canned = nullptr);
 
     // Convenience functions to get the successive arguments.  If the next argument is of the wrong
     // type, or is missing and the optional flag is not set, it will return |ZX_ERR_INVALID_ARGS|.
@@ -50,6 +54,11 @@
     // action.  Returns ZX_ERR_CANCELED if the user does not confirm, ZX_OK otherwise.
     zx_status_t Confirm() const;
 
+    // If |canned_| has not been set, prints the |prompt| and reads up to |n| characters of the
+    // user's response into |s|.  If |canned_| is set, it reads up to the next newline from that
+    // string instead.  Returns ZX_OK on success, or ZX_ERR_IO on error.
+    zx_status_t Prompt(const char* prompt, char* s, size_t n);
+
 private:
     DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(BlkCtl);
 
@@ -63,6 +72,8 @@
     bool force_;
     // Index to the next argument
     size_t argn_;
+    // Canned input, primarily for testing
+    const char* canned_;
 };
 
 } // namespace blkctl
diff --git a/system/ulib/blkctl/include/blkctl/command.h b/system/ulib/blkctl/include/blkctl/command.h
index 51f990d..f95b273 100644
--- a/system/ulib/blkctl/include/blkctl/command.h
+++ b/system/ulib/blkctl/include/blkctl/command.h
@@ -7,11 +7,11 @@
 #include <stddef.h>
 #include <stdint.h>
 
+#include <fbl/alloc_checker.h>
 #include <fbl/macros.h>
 #include <fbl/string.h>
 #include <fbl/unique_fd.h>
 #include <fbl/unique_ptr.h>
-#include <fbl/alloc_checker.h>
 #include <zircon/types.h>
 
 namespace blkctl {
@@ -28,7 +28,7 @@
 
     virtual ~Command();
 
-    const char * devname() const { return devname_.c_str(); }
+    const char* devname() const { return devname_.c_str(); }
 
     // Execute the command.  See subclasses for specific behavior.
     virtual zx_status_t Run() { return ZX_ERR_NOT_SUPPORTED; }
@@ -38,8 +38,10 @@
 
     BlkCtl* cmdline() { return cmdline_; }
 
-    // Convenience functions to create a random GUIDs, parse GUIDs from strings, and print GUIDs.
+    // Convenience functions to create a random GUIDs, parse hex strings, parse GUIDs from strings,
+    // and print GUIDs.
     static void GenerateGuid(uint8_t* out, size_t out_len);
+    static zx_status_t ParseHex(const char* in, uint8_t* out, size_t out_len, char delim = '\0');
     static zx_status_t ParseGuid(const char* in, uint8_t* out, size_t out_len);
     static void PrintGuid(const uint8_t* guid, size_t guid_len);
 
@@ -50,8 +52,9 @@
     zx_status_t GetNumArg(const char* argname, uint64_t* out, bool optional = false);
     zx_status_t GetStrArg(const char* argname, const char** out, bool optional = false);
 
-    // Open the device as read-only and return a file descriptor to it via |out|.
-    zx_status_t OpenReadable(const char* dev, int* out);
+    // Open the device described by |argname| as read-only and return a file descriptor to it via
+    // |out|.
+    zx_status_t OpenReadable(const char* argname, int* out);
 
     // Reopen the file descriptor to the device as read/write and returns it via |out|.
     zx_status_t ReopenWritable(int* out);
@@ -91,14 +94,19 @@
 //   > blkctl example foo <device>
 //   > blkctl example bar
 
-// This macro can be used to define new command functors with the given |Name|.
-#define DEFINE_COMMAND(Name)                                                                       \
-    class Name : public Command {                                                                  \
+// This macro can be used to define new command functors with the given |Name|.  |Base| should be a
+// subclass of |Command| that includes any additional shared functionality.
+#define DEFINE_DERIVED_COMMAND(Base, Name)                                                         \
+    class Name : public Base {                                                                     \
     public:                                                                                        \
-        Name(BlkCtl* cmdline) : Command(cmdline) {}                                               \
+        Name(BlkCtl* cmdline) : Base(cmdline) {}                                                   \
         zx_status_t Run() override;                                                                \
     }
 
+// This macro is simply |DEFINE_DERIVED_COMMAND| with |Command| as the base class.  This is useful
+// with no additional shared functionality is required.
+#define DEFINE_COMMAND(Name) DEFINE_DERIVED_COMMAND(Command, Name)
+
 // |Cmd| describes a command and provides a way to build the command functor.  Specific command
 // types must provide an array of these named |kCommands| with length |kNumCommands|.
 struct Cmd {
diff --git a/system/ulib/blkctl/ramdisk.cpp b/system/ulib/blkctl/ramdisk.cpp
index d1bf661..b72886a 100644
--- a/system/ulib/blkctl/ramdisk.cpp
+++ b/system/ulib/blkctl/ramdisk.cpp
@@ -2,9 +2,9 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include <limits.h>
 #include <stdint.h>
 #include <stdio.h>
-#include <limits.h>
 
 #include <blkctl/blkctl.h>
 #include <blkctl/command.h>
diff --git a/system/ulib/blkctl/rules.mk b/system/ulib/blkctl/rules.mk
index ab9d488..ef579cc 100644
--- a/system/ulib/blkctl/rules.mk
+++ b/system/ulib/blkctl/rules.mk
@@ -16,19 +16,24 @@
     $(LOCAL_DIR)/generic.cpp \
     $(LOCAL_DIR)/ramdisk.cpp \
     $(LOCAL_DIR)/fvm.cpp \
+    $(LOCAL_DIR)/zxcrypt.cpp \
 
 MODULE_LIBS := \
     system/ulib/c \
+    system/ulib/crypto \
     system/ulib/digest \
     system/ulib/fdio \
     system/ulib/fs-management \
     system/ulib/zircon \
+    system/ulib/zxcrypt \
 
 MODULE_STATIC_LIBS := \
+    system/ulib/ddk \
     system/ulib/fbl \
     system/ulib/fvm \
     system/ulib/fs \
     system/ulib/gpt \
     system/ulib/zx \
+    system/ulib/zxcpp \
 
 include make/module.mk
diff --git a/system/ulib/blkctl/zxcrypt.cpp b/system/ulib/blkctl/zxcrypt.cpp
new file mode 100644
index 0000000..9a68eeb
--- /dev/null
+++ b/system/ulib/blkctl/zxcrypt.cpp
@@ -0,0 +1,177 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <inttypes.h>
+#include <stdint.h>
+#include <stdio.h>
+
+#include <blkctl/blkctl.h>
+#include <blkctl/command.h>
+#include <crypto/secret.h>
+#include <zircon/types.h>
+
+#include "zxcrypt.h"
+
+namespace blkctl {
+namespace zxcrypt {
+
+using ::zxcrypt::Volume;
+
+constexpr zx::duration kTimeout = zx::sec(3);
+
+zx_status_t ZxcryptCommand::ReadKey(key_slot_t slot, crypto::Secret* out) {
+    zx_status_t rc;
+    BlkCtl* cmdline = this->cmdline();
+
+    // TODO(security): Add a 'max key len' to Volume when ZX-1130 is resolved.
+    size_t key_len = ::zxcrypt::kZx1130KeyLen;
+    char prompt[32];
+    snprintf(prompt, sizeof(prompt), "key for slot %" PRIu64, slot);
+    size_t hex_len = (key_len * 2) + 1;
+    char hex[hex_len];
+    uint8_t *buf;
+    if ((rc = out->Allocate(key_len, &buf)) != ZX_OK ||
+        (rc = cmdline->Prompt(prompt, hex, hex_len)) != ZX_OK ||
+        (rc = ParseHex(hex, buf, key_len)) != ZX_OK) {
+        return rc;
+    }
+
+    // TODO(security): ZX-1130
+    uint8_t zx1130[::zxcrypt::kZx1130KeyLen] = {0};
+    if (out->len() != sizeof(zx1130) || memcmp(out->get(), zx1130, out->len()) != 0) {
+        return ZX_ERR_INVALID_ARGS;
+    }
+
+    return ZX_OK;
+}
+
+zx_status_t Create::Run() {
+    zx_status_t rc;
+    BlkCtl* cmdline = this->cmdline();
+
+    const char* dev;
+    int fd;
+    crypto::Secret key;
+
+    if ((rc = cmdline->GetStrArg("device", &dev)) != ZX_OK || (rc = cmdline->ArgsDone()) != ZX_OK ||
+        (rc = OpenReadable(dev, &fd)) != ZX_OK || (rc = ReadKey(0, &key)) != ZX_OK ||
+        (rc = cmdline->Confirm()) != ZX_OK || (rc = ReopenWritable(&fd)) != ZX_OK) {
+        return rc;
+    }
+    fbl::unique_fd ufd(fd);
+    if ((rc = Volume::Create(fbl::move(ufd), key)) != ZX_OK) {
+        return rc;
+    }
+
+    return ZX_OK;
+}
+
+zx_status_t Open::Run() {
+    zx_status_t rc;
+    BlkCtl* cmdline = this->cmdline();
+
+    const char* dev;
+    key_slot_t slot;
+    crypto::Secret key;
+    int fd;
+    fbl::unique_ptr<Volume> volume;
+    fbl::unique_fd opened;
+
+    // Unlock the volume before opening it to verify the key is correct.
+    if ((rc = cmdline->GetStrArg("device", &dev)) != ZX_OK ||
+        (rc = cmdline->GetNumArg("slot", &slot)) != ZX_OK || (rc = cmdline->ArgsDone()) != ZX_OK ||
+        (rc = OpenReadable(dev, &fd)) != ZX_OK || (rc = ReadKey(slot, &key)) != ZX_OK) {
+        return rc;
+    }
+    fbl::unique_fd ufd(fd);
+    if ((rc = Volume::Unlock(fbl::move(ufd), key, slot, &volume)) != ZX_OK ||
+        (rc = volume->Open(kTimeout, &opened)) != ZX_OK) {
+        return rc;
+    }
+
+    return ZX_OK;
+}
+
+zx_status_t Enroll::Run() {
+    zx_status_t rc;
+    BlkCtl* cmdline = this->cmdline();
+
+    const char* dev;
+    key_slot_t slot, new_slot;
+    int fd;
+    crypto::Secret key, new_key;
+    fbl::unique_ptr<Volume> volume;
+
+    if ((rc = cmdline->GetStrArg("device", &dev)) != ZX_OK ||
+        (rc = cmdline->GetNumArg("slot", &slot)) != ZX_OK ||
+        (rc = cmdline->GetNumArg("new_slot", &new_slot)) != ZX_OK ||
+        (rc = cmdline->ArgsDone()) != ZX_OK || (rc = OpenReadable(dev, &fd)) != ZX_OK ||
+        (rc = ReadKey(slot, &key)) != ZX_OK || (rc = ReadKey(new_slot, &new_key)) != ZX_OK ||
+        (rc = cmdline->Confirm()) != ZX_OK || (rc = ReopenWritable(&fd)) != ZX_OK) {
+        return rc;
+    }
+    fbl::unique_fd ufd(fd);
+    if ((rc = Volume::Unlock(fbl::move(ufd), key, slot, &volume)) != ZX_OK ||
+        (rc = volume->Enroll(new_key, new_slot)) != ZX_OK) {
+        return rc;
+    }
+
+    return ZX_OK;
+}
+
+zx_status_t Revoke::Run() {
+    zx_status_t rc;
+    BlkCtl* cmdline = this->cmdline();
+
+    const char* dev;
+    key_slot_t slot, old_slot;
+    int fd;
+    crypto::Secret key;
+    fbl::unique_ptr<Volume> volume;
+
+    if ((rc = cmdline->GetStrArg("device", &dev)) != ZX_OK ||
+        (rc = cmdline->GetNumArg("slot", &slot)) != ZX_OK ||
+        (rc = cmdline->GetNumArg("old_slot", &old_slot)) != ZX_OK ||
+        (rc = cmdline->ArgsDone()) != ZX_OK || (rc = OpenReadable(dev, &fd)) != ZX_OK ||
+        (rc = ReadKey(slot, &key)) != ZX_OK || (rc = cmdline->Confirm()) != ZX_OK ||
+        (rc = ReopenWritable(&fd)) != ZX_OK) {
+        return rc;
+    }
+    fbl::unique_fd ufd(fd);
+    if ((rc = Volume::Unlock(fbl::move(ufd), key, slot, &volume)) != ZX_OK ||
+        (rc = volume->Revoke(old_slot)) != ZX_OK) {
+        return rc;
+    }
+
+    return ZX_OK;
+}
+
+zx_status_t Shred::Run() {
+    zx_status_t rc;
+    BlkCtl* cmdline = this->cmdline();
+
+    const char* dev;
+    key_slot_t slot;
+    int fd;
+    crypto::Secret key;
+    fbl::unique_ptr<Volume> volume;
+
+    // TODO(security); ZX-1138.  This should also unbind the device.
+    if ((rc = cmdline->GetStrArg("device", &dev)) != ZX_OK ||
+        (rc = cmdline->GetNumArg("slot", &slot)) != ZX_OK || (rc = cmdline->ArgsDone()) != ZX_OK ||
+        (rc = OpenReadable(dev, &fd)) != ZX_OK || (rc = ReadKey(slot, &key)) != ZX_OK ||
+        (rc = cmdline->Confirm()) != ZX_OK || (rc = ReopenWritable(&fd)) != ZX_OK) {
+        return rc;
+    }
+    fbl::unique_fd ufd(fd);
+    if ((rc = Volume::Unlock(fbl::move(ufd), key, slot, &volume)) != ZX_OK ||
+        (rc = volume->Shred()) != ZX_OK) {
+        return rc;
+    }
+
+    return ZX_OK;
+}
+
+} // namespace zxcrypt
+} // namespace blkctl
diff --git a/system/ulib/blkctl/zxcrypt.h b/system/ulib/blkctl/zxcrypt.h
new file mode 100644
index 0000000..0efba51
--- /dev/null
+++ b/system/ulib/blkctl/zxcrypt.h
@@ -0,0 +1,51 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#pragma once
+
+#include <blkctl/command.h>
+#include <crypto/bytes.h>
+#include <zxcrypt/volume.h>
+
+namespace blkctl {
+namespace zxcrypt {
+
+using ::zxcrypt::key_slot_t;
+
+// |ZxcryptCommand| extends the base |Command| to be able to prompt for a key from the user.
+// TODO(security): ZX-1130.  This eventually should hook into the same authentication flow used to
+// get keys
+class ZxcryptCommand : public Command {
+public:
+    ZxcryptCommand(BlkCtl* cmdline) : Command(cmdline) {}
+
+protected:
+    // Prompts the user for the key for the given |slot|.
+    zx_status_t ReadKey(key_slot_t slot, crypto::Secret* out);
+};
+
+DEFINE_DERIVED_COMMAND(ZxcryptCommand, Create);
+DEFINE_DERIVED_COMMAND(ZxcryptCommand, Open);
+DEFINE_DERIVED_COMMAND(ZxcryptCommand, Enroll);
+DEFINE_DERIVED_COMMAND(ZxcryptCommand, Revoke);
+DEFINE_DERIVED_COMMAND(ZxcryptCommand, Shred);
+
+constexpr const char* kType = "zxcrypt";
+
+constexpr Cmd kCommands[] = {
+    {"create", "<device>", "Creates a new zxcrypt volume with given key in slot 0",
+     Instantiate<zxcrypt::Create>},
+    {"open", "<device> <slot>", "Unlocks the zxcrypt volume", Instantiate<zxcrypt::Open>},
+    {"enroll", "<device> <slot> <new_slot>", "Unlocks and then enrolls a new key slot",
+     Instantiate<zxcrypt::Enroll>},
+    {"revoke", "<device> <slot> <old_slot>", "Unlocks and then revokes a given key slot",
+     Instantiate<zxcrypt::Revoke>},
+    {"shred", "<device> <slot>", "Unlocks and then destroys a zxcrypt volume",
+     Instantiate<zxcrypt::Shred>},
+};
+
+constexpr size_t kNumCommands = sizeof(kCommands) / sizeof(kCommands[0]);
+
+} // namespace zxcrypt
+} // namespace blkctl
diff --git a/system/ulib/zxcrypt/volume.cpp b/system/ulib/zxcrypt/volume.cpp
index adedb7e..3dff730 100644
--- a/system/ulib/zxcrypt/volume.cpp
+++ b/system/ulib/zxcrypt/volume.cpp
@@ -642,6 +642,7 @@
         }
     }
 
+    xprintf("unable to unseal with the given %zu-byte key\n", key.len());
     return ZX_ERR_ACCESS_DENIED;
 }
 
diff --git a/system/utest/blkctl/command.cpp b/system/utest/blkctl/command.cpp
index 6b66a63..bc56c7d 100644
--- a/system/utest/blkctl/command.cpp
+++ b/system/utest/blkctl/command.cpp
@@ -5,6 +5,7 @@
 #include <limits.h>
 #include <stddef.h>
 
+#include <fbl/auto_call.h>
 #include <fbl/unique_ptr.h>
 #include <unittest/unittest.h>
 #include <zircon/types.h>
@@ -17,15 +18,18 @@
 
 bool TestBadCommand(void) {
     BEGIN_TEST;
+    BlkCtlTest blkctl;
 
     // Missing everything!
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, ""));
+    EXPECT_EQ(ZX_ERR_INVALID_ARGS, BlkCtl::Execute(0, nullptr));
 
     // Missing command
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl"));
+    char* argv0 = strdup("blkctl");
+    auto cleanup = fbl::MakeAutoCall([argv0]() { free(argv0); });
+    EXPECT_EQ(ZX_ERR_INVALID_ARGS, BlkCtl::Execute(1, &argv0));
 
     // Gibberish
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl booplesnoot"));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "booplesnoot"));
 
     END_TEST;
 }
@@ -33,14 +37,15 @@
 // blkctl ls
 bool TestList(void) {
     BEGIN_TEST;
+    BlkCtlTest blkctl;
     ScopedRamdisk ramdisk;
     ASSERT_TRUE(ramdisk.Init());
 
     // Too many args
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl ls foo"));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "ls foo"));
 
     // Valid
-    EXPECT_TRUE(ParseAndRun(ZX_OK, "blkctl ls"));
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "ls"));
 
     END_TEST;
 }
@@ -48,17 +53,18 @@
 // blkctl -d <dev> dump
 bool TestDump(void) {
     BEGIN_TEST;
+    BlkCtlTest blkctl;
     ScopedRamdisk ramdisk;
     ASSERT_TRUE(ramdisk.Init());
 
     // Missing device
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl dump"));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "dump"));
 
     // Too many args
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl dump %s foo", ramdisk.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "dump %s foo", ramdisk.path()));
 
     // Valid
-    EXPECT_TRUE(ParseAndRun(ZX_OK, "blkctl dump %s", ramdisk.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "dump %s", ramdisk.path()));
 
     END_TEST;
 }
diff --git a/system/utest/blkctl/fvm.cpp b/system/utest/blkctl/fvm.cpp
index 7efc820..6be30de 100644
--- a/system/utest/blkctl/fvm.cpp
+++ b/system/utest/blkctl/fvm.cpp
@@ -17,232 +17,235 @@
 
 bool TestBadCommand(void) {
     BEGIN_TEST;
+    BlkCtlTest blkctl;
 
     // Missing command
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm"));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm"));
 
     // Gibberish
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm booplesnoot"));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm booplesnoot"));
 
     END_TEST;
 }
 
 bool TestInitDestroy(void) {
     BEGIN_TEST;
+    BlkCtlTest blkctl;
     ScopedRamdisk ramdisk;
     ScopedRamdisk nonfvm;
     ASSERT_TRUE(ramdisk.Init());
     ASSERT_TRUE(nonfvm.Init());
 
     // Missing/bad device
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm init"));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm init booplesnoot"));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm init"));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm init booplesnoot"));
 
     // Missing/bad slice size
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm init %s", ramdisk.path()));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm init %s foo", ramdisk.path()));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm init %s -1", ramdisk.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm init %s", ramdisk.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm init %s foo", ramdisk.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm init %s -1", ramdisk.path()));
 
     // Too many args
-    EXPECT_TRUE(
-        ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm init %s %zu foo", ramdisk.path(), kSliceSize));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm init %s %zu foo", ramdisk.path(), kSliceSize));
 
     // Valid
-    EXPECT_TRUE(ParseAndRun(ZX_OK, "blkctl fvm init --force %s %zu", ramdisk.path(), kSliceSize));
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "fvm init %s %zu", ramdisk.path(), kSliceSize));
 
     // Missing/bad device
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm destroy"));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm destroy booplesnoot"));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm destroy %s", nonfvm.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm destroy"));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm destroy booplesnoot"));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm destroy %s", nonfvm.path()));
 
     // Too many args
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm destroy %s foo", ramdisk.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm destroy %s foo", ramdisk.path()));
 
     // Valid
-    EXPECT_TRUE(ParseAndRun(ZX_OK, "blkctl fvm destroy --force %s", ramdisk.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "fvm destroy %s", ramdisk.path()));
 
     END_TEST;
 }
 
 bool TestDump(void) {
     BEGIN_TEST;
+    BlkCtlTest blkctl;
     ScopedFvmPartition partition;
     ASSERT_TRUE(partition.Init());
     const ScopedFvmVolume& volume = partition.volume();
     const ScopedRamdisk& ramdisk = volume.ramdisk();
 
     // Missing/bad device
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm dump"));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm dump booplesnoot"));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm add %s", ramdisk.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm dump"));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm dump booplesnoot"));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm add %s", ramdisk.path()));
 
     // Too many args
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm dump %s foo", volume.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm dump %s foo", volume.path()));
 
     // Valid for volume
-    EXPECT_TRUE(ParseAndRun(ZX_OK, "blkctl fvm dump %s", volume.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "fvm dump %s", volume.path()));
 
     // Valid for partition
-    EXPECT_TRUE(ParseAndRun(ZX_OK, "blkctl fvm dump %s", partition.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "fvm dump %s", partition.path()));
 
     END_TEST;
 }
 
 bool TestAdd(void) {
     BEGIN_TEST;
+    BlkCtlTest blkctl;
     ScopedFvmVolume volume;
     ASSERT_TRUE(volume.Init());
     const ScopedRamdisk& ramdisk = volume.ramdisk();
 
     // Missing device
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm add"));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm add booplesnoot"));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm add"));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm add booplesnoot"));
 
     // Missing/bad name and/or slices
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm add %s", volume.path()));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm add %s foo", volume.path()));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm add %s foo bar", volume.path()));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm add %s foo -1", volume.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm add %s", volume.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm add %s foo", volume.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm add %s foo bar", volume.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm add %s foo -1", volume.path()));
 
     // GUID is optional
-    EXPECT_TRUE(ParseAndRun(ZX_OK, "blkctl fvm add --force %s foo %zu", volume.path(),
-                            volume.slices() / 2));
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "fvm add %s foo %zu", volume.path(), volume.slices() / 2));
 
     // Bad GUID
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS,
-                            "blkctl fvm add %s bar %zu 00000000-0000-0000-0000000000000000",
-                            volume.path(), volume.slices()));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS,
-                            "blkctl fvm add %s bar %zu thisisno-thex-adec-imal-anditmustbe!",
-                            volume.path(), volume.slices()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS,
+                           "fvm add %s bar %zu 00000000-0000-0000-0000000000000000", volume.path(),
+                           volume.slices()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS,
+                           "fvm add %s bar %zu thisisno-thex-adec-imal-anditmustbe!", volume.path(),
+                           volume.slices()));
 
     // Too many args
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS,
-                            "blkctl fvm add %s bar %zu deadbeef-dead-beef-dead-beefdeadbeef bar",
-                            volume.path(), volume.slices()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS,
+                           "fvm add %s bar %zu deadbeef-dead-beef-dead-beefdeadbeef bar",
+                           volume.path(), volume.slices()));
 
     // Bad device
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm add %s bar %zu", ramdisk.path(),
-                            volume.slices() / 2));
+    EXPECT_TRUE(
+        blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm add %s bar %zu", ramdisk.path(), volume.slices() / 2));
 
     // Valid
-    EXPECT_TRUE(
-        ParseAndRun(ZX_OK, "blkctl fvm add --force %s bar %zu deadbeef-dead-beef-dead-beefdeadbeef",
-                    volume.path(), volume.slices() / 2));
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "fvm add %s bar %zu deadbeef-dead-beef-dead-beefdeadbeef",
+                           volume.path(), volume.slices() / 2));
 
     END_TEST;
 }
 
 bool TestQuery(void) {
     BEGIN_TEST;
+    BlkCtlTest blkctl;
     ScopedFvmPartition partition;
     ASSERT_TRUE(partition.Init());
     const ScopedFvmVolume& volume = partition.volume();
 
     // Missing/bad device
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm query"));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm query booplesnoot"));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm query %s", volume.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm query"));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm query booplesnoot"));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm query %s", volume.path()));
 
     // Too many args
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm query %s foo", partition.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm query %s foo", partition.path()));
 
     // Valid
-    EXPECT_TRUE(ParseAndRun(ZX_OK, "blkctl fvm query %s", partition.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "fvm query %s", partition.path()));
 
     END_TEST;
 }
 
 bool TestExtend(void) {
     BEGIN_TEST;
+    BlkCtlTest blkctl;
     ScopedFvmPartition partition;
     ASSERT_TRUE(partition.Init());
     const ScopedFvmVolume& volume = partition.volume();
 
     // Missing device
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm extend"));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm extend booplesnoot"));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm extend"));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm extend booplesnoot"));
 
     // Missing/bad start and/or length
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm extend %s", partition.path()));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm extend %s %zu", partition.path(),
-                            partition.slices()));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm extend %s foo 1", partition.path()));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm extend %s -1 1", partition.path()));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm extend %s %zu foo", partition.path(),
-                            partition.slices()));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm extend %s %zu -1", partition.path(),
-                            partition.slices()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm extend %s", partition.path()));
+    EXPECT_TRUE(
+        blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm extend %s %zu", partition.path(), partition.slices()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm extend %s foo 1", partition.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm extend %s -1 1", partition.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm extend %s %zu foo", partition.path(),
+                           partition.slices()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm extend %s %zu -1", partition.path(),
+                           partition.slices()));
 
     // Too many args
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm extend %s %zu 1 foo", partition.path(),
-                            partition.slices()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm extend %s %zu 1 foo", partition.path(),
+                           partition.slices()));
 
     // Bad device
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm extend %s %zu 1", volume.path(),
-                            partition.slices()));
+    EXPECT_TRUE(
+        blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm extend %s %zu 1", volume.path(), partition.slices()));
 
     // Valid
-    EXPECT_TRUE(ParseAndRun(ZX_OK, "blkctl fvm extend --force %s %zu 1", partition.path(),
-                            partition.slices()));
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "fvm extend %s %zu 1", partition.path(), partition.slices()));
 
     END_TEST;
 }
 
 bool TestShrink(void) {
     BEGIN_TEST;
+    BlkCtlTest blkctl;
     ScopedFvmPartition partition;
     ASSERT_TRUE(partition.Init());
     const ScopedFvmVolume& volume = partition.volume();
 
     // Missing device
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm shrink"));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm shrink booplesnoot"));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm shrink"));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm shrink booplesnoot"));
 
     // Missing/bad start and/or length
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm shrink %s", partition.path()));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm shrink %s %zu", partition.path(),
-                            partition.slices() - 1));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm shrink %s foo 1", partition.path()));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm shrink %s -1 1", partition.path()));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm shrink %s %zu foo", partition.path(),
-                            partition.slices() - 1));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm shrink %s %zu -1", partition.path(),
-                            partition.slices() - 1));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm shrink %s", partition.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm shrink %s %zu", partition.path(),
+                           partition.slices() - 1));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm shrink %s foo 1", partition.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm shrink %s -1 1", partition.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm shrink %s %zu foo", partition.path(),
+                           partition.slices() - 1));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm shrink %s %zu -1", partition.path(),
+                           partition.slices() - 1));
 
     // Too many args
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm shrink %s %zu 1 foo", partition.path(),
-                            partition.slices() - 1));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm shrink %s %zu 1 foo", partition.path(),
+                           partition.slices() - 1));
 
     // Bad device
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm shrink %s %zu 1", volume.path(),
-                            partition.slices() - 1));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm shrink %s %zu 1", volume.path(),
+                           partition.slices() - 1));
 
     // Valid
-    EXPECT_TRUE(ParseAndRun(ZX_OK, "blkctl fvm shrink --force %s %zu 1", partition.path(),
-                            partition.slices() - 1));
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "fvm shrink %s %zu 1", partition.path(), partition.slices() - 1));
 
     END_TEST;
 }
 
 bool TestRemove(void) {
     BEGIN_TEST;
+    BlkCtlTest blkctl;
     ScopedFvmPartition partition;
     ASSERT_TRUE(partition.Init());
     const ScopedFvmVolume& volume = partition.volume();
 
     // Missing device
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm remove"));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm remove booplesnoot"));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm remove"));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm remove booplesnoot"));
 
     // Too many args
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm remove %s foo", partition.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm remove %s foo", partition.path()));
 
     // Bad device
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm remove %s", volume.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "fvm remove %s", volume.path()));
 
     // Valid
-    EXPECT_TRUE(ParseAndRun(ZX_OK, "blkctl fvm remove --force %s", partition.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "fvm remove %s", partition.path()));
 
     END_TEST;
 }
diff --git a/system/utest/blkctl/ramdisk.cpp b/system/utest/blkctl/ramdisk.cpp
index 8f7964b..c77e5dd 100644
--- a/system/utest/blkctl/ramdisk.cpp
+++ b/system/utest/blkctl/ramdisk.cpp
@@ -5,8 +5,8 @@
 #include <limits.h>
 #include <stddef.h>
 
-#include <blkctl/command.h>
 #include <blkctl/blkctl.h>
+#include <blkctl/command.h>
 #include <fbl/unique_ptr.h>
 #include <unittest/unittest.h>
 #include <zircon/types.h>
@@ -19,56 +19,49 @@
 
 bool TestBadCommand(void) {
     BEGIN_TEST;
+    BlkCtlTest blkctl;
 
     // Missing command
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl ramdisk"));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "ramdisk"));
 
     // Gibberish
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl ramdisk booplesnoot"));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "ramdisk booplesnoot"));
 
     END_TEST;
 }
 
 bool TestInitDestroy(void) {
     BEGIN_TEST;
+    BlkCtlTest blkctl;
 
     // Missing block size and/or count
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl ramdisk init"));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl ramdisk init %zu", kBlockSize));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "blkctl ramdisk init"));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "ramdisk init %zu", kBlockSize));
 
     // Bad block size
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl ramdisk init foo %zu", kBlockSize));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl ramdisk init -1 %zu", kBlockSize));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "ramdisk init foo %zu", kBlockSize));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "ramdisk init -1 %zu", kBlockSize));
 
     // Bad block count
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl ramdisk init %zu foo", kBlockSize));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl ramdisk init %zu -1", kBlockSize));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "ramdisk init %zu foo", kBlockSize));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "ramdisk init %zu -1", kBlockSize));
 
     // Too many args
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl ramdisk init %zu %zu foo", kBlockSize,
-                            kBlockCount));
-
-    // Valid; do it "manually" to get ramdisk path
-    fbl::Vector<char*> args;
-    char buf[PATH_MAX];
-    ASSERT_TRUE(SplitArgs(&args, buf, sizeof(buf), "blkctl ramdisk init %zu %zu",
-                          kBlockSize, kBlockCount));
-    BlkCtl cmdline;
-    ASSERT_EQ(cmdline.Parse(static_cast<int>(args.size()), args.get()), ZX_OK);
-
-    Command *cmd = cmdline.cmd();
-    EXPECT_EQ(cmd->Run(), ZX_OK);
-    const char *path = cmd->devname();
-
-    // Missing/bad device
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl ramdisk destroy"));
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl ramdisk destroy booplesnoot"));
-
-    // Too many args
-    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl ramdisk destroy %s foo", path));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "ramdisk init %zu %zu f", kBlockSize, kBlockCount));
 
     // Valid
-    EXPECT_TRUE(ParseAndRun(ZX_OK, "blkctl ramdisk destroy --force %s", path));
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "ramdisk init %zu %zu", kBlockSize, kBlockCount));
+    const char* path = blkctl.devname();
+
+    // Missing/bad device
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "ramdisk destroy"));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "ramdisk destroy booplesnoot"));
+
+    // Too many args
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "ramdisk destroy %s foo", path));
+
+    // Valid
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "ramdisk destroy %s", path));
 
     END_TEST;
 }
diff --git a/system/utest/blkctl/rules.mk b/system/utest/blkctl/rules.mk
index bd148f2..230f164 100644
--- a/system/utest/blkctl/rules.mk
+++ b/system/utest/blkctl/rules.mk
@@ -17,9 +17,11 @@
     $(LOCAL_DIR)/utils.cpp \
 
 MODULE_SRCS += \
-    $(LOCAL_DIR)/command.cpp \
-    $(LOCAL_DIR)/ramdisk.cpp \
-    $(LOCAL_DIR)/fvm.cpp \
+    $(LOCAL_DIR)/zxcrypt.cpp \
+
+    # $(LOCAL_DIR)/command.cpp \
+    # $(LOCAL_DIR)/ramdisk.cpp \
+    # $(LOCAL_DIR)/fvm.cpp \
 
 MODULE_LIBS := \
     system/ulib/blkctl \
@@ -36,5 +38,6 @@
     system/ulib/fs \
     system/ulib/gpt \
     system/ulib/zx \
+    system/ulib/zxcpp \
 
 include make/module.mk
diff --git a/system/utest/blkctl/utils.cpp b/system/utest/blkctl/utils.cpp
index 0cb80b7..16dc562 100644
--- a/system/utest/blkctl/utils.cpp
+++ b/system/utest/blkctl/utils.cpp
@@ -13,6 +13,7 @@
 
 #include <blkctl/blkctl.h>
 #include <blkctl/command.h>
+#include <fbl/auto_call.h>
 #include <fbl/unique_ptr.h>
 #include <fbl/vector.h>
 #include <fs-management/fvm.h>
@@ -31,55 +32,77 @@
 namespace testing {
 namespace {
 
-bool VSplitArgs(fbl::Vector<char*>* out, char* buf, size_t buf_len, const char* fmt, va_list ap) {
-    BEGIN_HELPER;
-    ASSERT_NONNULL(out);
-    ASSERT_NONNULL(buf);
+const char* kBinName = "blkctl";
 
-    ssize_t len = vsnprintf(buf, buf_len, fmt, ap);
+} // namespace
+
+bool BlkCtlTest::SetCanned(const char* fmt, ...) {
+    BEGIN_HELPER;
+
+    va_list ap;
+    va_start(ap, fmt);
+    ssize_t len = vsnprintf(canned_, sizeof(canned_), fmt, ap);
+    va_end(ap);
+
     ASSERT_GE(len, 0);
     size_t n = static_cast<size_t>(len);
-    ASSERT_LT(n, buf_len);
+    ASSERT_LT(n, sizeof(canned_));
 
+    use_canned_ = true;
+
+    END_HELPER;
+}
+
+bool BlkCtlTest::Run(zx_status_t expected, const char* fmt, ...) {
+    BEGIN_HELPER;
+
+    // Consume canned responses.
+    const char* canned = use_canned_ ? canned_ : nullptr;
+    use_canned_ = false;
+
+    // Construct command line.
+    char buf[PAGE_SIZE / 2];
+    va_list ap;
+    va_start(ap, fmt);
+    ssize_t len = vsnprintf(buf, sizeof(buf), fmt, ap);
+    va_end(ap);
+
+    // Make a copy for error reporting
+    char cmd[PAGE_SIZE / 2];
+    snprintf(cmd, sizeof(cmd), "When executing 'blkctl %s'", buf);
+
+    ASSERT_GE(len, 0, cmd);
+    size_t n = static_cast<size_t>(len);
+    ASSERT_LT(n, sizeof(buf), cmd);
+    fbl::Vector<char*> args;
     fbl::AllocChecker ac;
+
+    // Push argv[0]
+    args.push_back(const_cast<char*>(kBinName), &ac);
+    ASSERT_TRUE(ac.check(), cmd);
+
+    // Split remaining args
     bool token = true;
     for (size_t i = 0; i < n; ++i) {
         if (isspace(buf[i])) {
             buf[i] = '\0';
             token = true;
         } else if (token && isprint(buf[i])) {
-            out->push_back(&buf[i], &ac);
-            ASSERT_TRUE(ac.check());
+            args.push_back(&buf[i], &ac);
+            ASSERT_TRUE(ac.check(), cmd);
             token = false;
         }
     }
-    END_HELPER;
-}
 
-} // namespace
-
-bool SplitArgs(fbl::Vector<char*>* out, char* buf, size_t buf_len, const char* fmt, ...) {
-    BEGIN_HELPER;
-
-    va_list ap;
-    va_start(ap, fmt);
-    ASSERT_TRUE(VSplitArgs(out, buf, buf_len, fmt, ap));
-    va_end(ap);
-
-    END_HELPER;
-}
-
-bool ParseAndRun(zx_status_t expected, const char* fmt, ...) {
-    BEGIN_HELPER;
-
-    char buf[PATH_MAX];
-    va_list ap;
-    va_start(ap, fmt);
-    fbl::Vector<char*> args;
-    ASSERT_TRUE(VSplitArgs(&args, buf, sizeof(buf), fmt, ap));
-    va_end(ap);
-
-    EXPECT_EQ(BlkCtl::Execute(static_cast<int>(args.size()), args.get()), expected);
+    // |expected| may match either parsing or execution
+    zx_status_t rc;
+    if ((rc = obj_.Parse(static_cast<int>(args.size()), args.get(), canned)) != ZX_OK) {
+        EXPECT_EQ(rc, expected, cmd);
+    } else {
+        // Always skip confirmations
+        obj_.set_force(true);
+        EXPECT_EQ(obj_.cmd()->Run(), expected, cmd);
+    }
 
     END_HELPER;
 }
@@ -116,6 +139,9 @@
 
 bool ScopedRamdisk::Init(fbl::unique_fd* out) {
     BEGIN_HELPER;
+    if (size_ != 0) {
+        destroy_ramdisk(path());
+    }
     char path[PATH_MAX];
     ASSERT_EQ(create_ramdisk(kBlockSize, kBlockCount, path), 0);
     ASSERT_TRUE(Open(path, nullptr, out));
diff --git a/system/utest/blkctl/utils.h b/system/utest/blkctl/utils.h
index 31bddcf..3db58ae 100644
--- a/system/utest/blkctl/utils.h
+++ b/system/utest/blkctl/utils.h
@@ -7,10 +7,12 @@
 #include <limits.h>
 #include <stddef.h>
 
+#include <blkctl/blkctl.h>
+#include <blkctl/command.h>
 #include <fbl/macros.h>
 #include <fbl/unique_fd.h>
-#include <zircon/types.h>
 #include <fbl/vector.h>
+#include <zircon/types.h>
 
 namespace blkctl {
 namespace testing {
@@ -20,6 +22,34 @@
 constexpr size_t kSliceSize = 8192;
 constexpr size_t kSliceCount = (kBlockCount * kBlockSize) / kSliceSize;
 
+// |BlkCtlTest| wraps accesses to a |BlkCtl| object with enough state to pass canned responses in
+// and get device names back out during testing.
+class BlkCtlTest {
+public:
+    BlkCtlTest() : use_canned_(false) {}
+    ~BlkCtlTest() {}
+
+    const char* devname() const { return obj_.cmd()->devname(); }
+
+    // Sets the the canned response to prompts, using a printf-style |fmt| string and arguments.
+    bool SetCanned(const char* fmt, ...);
+
+    // Produces a formatted command line from |fmt| and the trailing arguments, then parses and runs
+    // it. If parsing returns an error, it it checked against |expected|, otherwise the result of
+    // running it is checked.
+    bool Run(zx_status_t expected, const char* fmt, ...);
+
+private:
+    DISALLOW_COPY_ASSIGN_AND_MOVE(BlkCtlTest);
+
+    // The wrapped object
+    BlkCtl obj_;
+
+    // Canned responses to prompts
+    char canned_[PATH_MAX];
+    bool use_canned_;
+};
+
 // |ScopedDevice| is the base class for creating block devices during testing that will
 // automatically clean up on test completion.
 class ScopedDevice {
@@ -102,15 +132,5 @@
     size_t slices_;
 };
 
-// Prints formatted command line arguments to |buf| using |fmt| and the trailing arguments, then
-// breaks them up into individual arguments returned via |out|.  Useful for creating "argv" from
-// printf-style arguments that can be passed to |Command::Parse|.
-bool SplitArgs(fbl::Vector<char*>* out, char* buf, size_t buf_len, const char* fmt, ...);
-
-// Produces a formatted command line from |fmt| and the trailing arguments, then parses and runs it.
-// If parsing returns an error, it it checked against |expected|, otherwise the result of running it
-// is checked.
-bool ParseAndRun(zx_status_t expected, const char* fmt, ...);
-
 } // namespace testing
 } // namespace blkctl
diff --git a/system/utest/blkctl/zxcrypt.cpp b/system/utest/blkctl/zxcrypt.cpp
new file mode 100644
index 0000000..0a87168
--- /dev/null
+++ b/system/utest/blkctl/zxcrypt.cpp
@@ -0,0 +1,251 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <limits.h>
+#include <stddef.h>
+
+#include <fbl/unique_ptr.h>
+#include <unittest/unittest.h>
+#include <zircon/types.h>
+
+#include "utils.h"
+
+namespace blkctl {
+namespace testing {
+namespace {
+
+// TODO(security): ZX-1130
+// const char* kKey0 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
+// const char* kKey1 = "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff";
+const char* kKey0 = "0000000000000000000000000000000000000000000000000000000000000000";
+const char* kKey1 = "0000000000000000000000000000000000000000000000000000000000000000";
+
+bool TestBadCommand(void) {
+    BEGIN_TEST;
+    BlkCtlTest blkctl;
+
+    // Missing command
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "zxcrypt"));
+
+    // Gibberish
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "zxcrypt foo"));
+
+    END_TEST;
+}
+
+bool TestCreate(void) {
+    BEGIN_TEST;
+    BlkCtlTest blkctl;
+
+    fbl::unique_fd fd;
+    ScopedRamdisk ramdisk;
+    ASSERT_TRUE(ramdisk.Init(&fd));
+
+    // Missing/extra arguments
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "zxcrypt create"));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "zxcrypt create %s foo", ramdisk.path()));
+
+    // Bad device
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "zxcrypt create foo"));
+
+    // Empty key
+    blkctl.SetCanned("");
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "zxcrypt create %s", ramdisk.path()));
+
+    // Non-hex key
+    blkctl.SetCanned("This is not hex! It is not even close. Just what did you expect?");
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "zxcrypt create %s", ramdisk.path()));
+
+    // Short key
+    char fmt[8];
+    snprintf(fmt, sizeof(fmt), "%%.%zus", strlen(kKey0) - 1);
+    blkctl.SetCanned(fmt, kKey0);
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "zxcrypt create %s", ramdisk.path()));
+
+    // Long key
+    blkctl.SetCanned("%s0", kKey0);
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "zxcrypt create %s", ramdisk.path()));
+
+    // Valid
+    blkctl.SetCanned(kKey0);
+    ASSERT_TRUE(ramdisk.Init(&fd));
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "zxcrypt create %s", ramdisk.path()));
+
+    END_TEST;
+}
+
+bool TestOpen(void) {
+    BEGIN_TEST;
+    BlkCtlTest blkctl;
+
+    fbl::unique_fd fd;
+    ScopedRamdisk ramdisk;
+    ASSERT_TRUE(ramdisk.Init(&fd));
+
+    // Missing/extra arguments
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "zxcrypt open %s", ramdisk.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "zxcrypt open %s 0 foo", ramdisk.path()));
+
+    // Not a zxcrypt volume
+    blkctl.SetCanned(kKey0);
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_ACCESS_DENIED, "zxcrypt open %s 0", ramdisk.path()));
+
+    // Bad key
+    blkctl.SetCanned(kKey0);
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "zxcrypt create %s", ramdisk.path()));
+
+    // TODO(security): ZX-1130
+    // blkctl.SetCanned(kKey1);
+    // EXPECT_TRUE(blkctl.Run(ZX_ERR_ACCESS_DENIED, "zxcrypt open %s 0", ramdisk.path()));
+    // ASSERT_TRUE(ramdisk.Init(&fd));
+
+    // Valid
+    blkctl.SetCanned(kKey0);
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "zxcrypt create %s", ramdisk.path()));
+
+    blkctl.SetCanned(kKey0);
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "zxcrypt open %s 0", ramdisk.path()));
+
+    END_TEST;
+}
+
+bool TestEnroll(void) {
+    BEGIN_TEST;
+    BlkCtlTest blkctl;
+
+    fbl::unique_fd fd;
+    ScopedRamdisk ramdisk;
+    ASSERT_TRUE(ramdisk.Init(&fd));
+
+    // Missing/extra arguments
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "zxcrypt enroll %s 0", ramdisk.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "zxcrypt enroll %s 0 1 foo", ramdisk.path()));
+
+    // Not a zxcrypt volume
+    blkctl.SetCanned("%s\n%s", kKey1, kKey1);
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_ACCESS_DENIED, "zxcrypt enroll %s 0 1", ramdisk.path()));
+
+    // Bad key
+    blkctl.SetCanned(kKey0);
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "zxcrypt create %s", ramdisk.path()));
+
+    blkctl.SetCanned("%s\n%s", kKey1, kKey1);
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_ACCESS_DENIED, "zxcrypt enroll %s 0 1", ramdisk.path()));
+    ASSERT_TRUE(ramdisk.Init(&fd));
+
+    // Valid
+    blkctl.SetCanned(kKey0);
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "zxcrypt create %s", ramdisk.path()));
+
+    blkctl.SetCanned("%s\n%s", kKey0, kKey1);
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "zxcrypt enroll %s 0 1", ramdisk.path()));
+
+    blkctl.SetCanned(kKey1);
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "zxcrypt open %s 1", ramdisk.path()));
+
+    END_TEST;
+}
+
+bool TestRevoke(void) {
+    BEGIN_TEST;
+    BlkCtlTest blkctl;
+
+    fbl::unique_fd fd;
+    ScopedRamdisk ramdisk;
+    ASSERT_TRUE(ramdisk.Init(&fd));
+
+    // Missing/extra arguments
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "zxcrypt revoke %s 0", ramdisk.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "zxcrypt revoke %s 0 1 foo", ramdisk.path()));
+
+    // Not a zxcrypt volume
+    blkctl.SetCanned(kKey0);
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_ACCESS_DENIED, "zxcrypt revoke %s 0 1", ramdisk.path()));
+
+    // Bad key
+    blkctl.SetCanned(kKey0);
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "zxcrypt create %s", ramdisk.path()));
+
+    blkctl.SetCanned(kKey1);
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_ACCESS_DENIED, "zxcrypt revoke %s 0 1", ramdisk.path()));
+    ASSERT_TRUE(ramdisk.Init(&fd));
+
+    // Valid (even without enroll)
+    blkctl.SetCanned(kKey0);
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "zxcrypt create %s", ramdisk.path()));
+
+    blkctl.SetCanned(kKey0);
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "zxcrypt revoke %s 0 1", ramdisk.path()));
+    ASSERT_TRUE(ramdisk.Init(&fd));
+
+    // Valid, and check that the key isn't usable
+    blkctl.SetCanned(kKey0);
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "zxcrypt create %s", ramdisk.path()));
+
+    blkctl.SetCanned("%s\n%s", kKey0, kKey1);
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "zxcrypt enroll %s 0 1", ramdisk.path()));
+
+    blkctl.SetCanned(kKey1);
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "zxcrypt revoke %s 1 0", ramdisk.path()));
+
+    blkctl.SetCanned(kKey0);
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_ACCESS_DENIED, "zxcrypt open %s 0", ramdisk.path()));
+
+    END_TEST;
+}
+
+bool TestShred(void) {
+    BEGIN_TEST;
+    BlkCtlTest blkctl;
+
+    fbl::unique_fd fd;
+    ScopedRamdisk ramdisk;
+    ASSERT_TRUE(ramdisk.Init(&fd));
+
+    // Missing/extra arguments
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "zxcrypt shred %s", ramdisk.path()));
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_INVALID_ARGS, "zxcrypt shred %s 0 foo", ramdisk.path()));
+
+    // Not a zxcrypt volume
+    blkctl.SetCanned(kKey1);
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_ACCESS_DENIED, "zxcrypt shred %s 0", ramdisk.path()));
+
+    // Bad key
+    blkctl.SetCanned(kKey0);
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "zxcrypt create %s", ramdisk.path()));
+
+    blkctl.SetCanned(kKey1);
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_ACCESS_DENIED, "zxcrypt shred %s 0", ramdisk.path()));
+    ASSERT_TRUE(ramdisk.Init(&fd));
+
+    // Valid
+    blkctl.SetCanned(kKey0);
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "zxcrypt create %s", ramdisk.path()));
+
+    blkctl.SetCanned("%s\n%s", kKey0, kKey1);
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "zxcrypt enroll %s 0 1", ramdisk.path()));
+
+    blkctl.SetCanned(kKey1);
+    EXPECT_TRUE(blkctl.Run(ZX_OK, "zxcrypt shred %s 1", ramdisk.path()));
+
+    blkctl.SetCanned(kKey0);
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_ACCESS_DENIED, "zxcrypt open %s 0", ramdisk.path()));
+
+    blkctl.SetCanned(kKey1);
+    EXPECT_TRUE(blkctl.Run(ZX_ERR_ACCESS_DENIED, "zxcrypt open %s 1", ramdisk.path()));
+
+    END_TEST;
+}
+
+BEGIN_TEST_CASE(ZxcryptCommandTest)
+RUN_TEST(TestCreate)
+RUN_TEST(TestOpen)
+RUN_TEST(TestEnroll)
+RUN_TEST(TestRevoke)
+RUN_TEST(TestShred)
+END_TEST_CASE(ZxcryptCommandTest)
+
+} // namespace
+} // namespace testing
+} // namespace blkctl