]> Cypherpunks.ru repositories - nncp.git/commitdiff
Encrypted configuration file
authorSergey Matveev <stargrave@stargrave.org>
Sun, 30 Apr 2017 09:20:41 +0000 (12:20 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Sun, 30 Apr 2017 13:48:58 +0000 (16:48 +0300)
15 files changed:
.gitmodules
common.mk
doc/cmds.texi
doc/download.texi
doc/eblob.texi [new file with mode: 0644]
doc/index.texi
doc/news.ru.texi
doc/news.texi
doc/workflow.texi
makedist.sh
ports/nncp/Makefile
src/cypherpunks.ru/balloon [new submodule]
src/cypherpunks.ru/nncp/cfg.go
src/cypherpunks.ru/nncp/cmd/nncp-cfgenc/main.go [new file with mode: 0644]
src/cypherpunks.ru/nncp/eblob.go [new file with mode: 0644]

index ea445fbb69d73a850172f64a1ea841ff044bc8b9..ae68d8ec6d28eccfec894dc578a0059dd8175e6b 100644 (file)
@@ -30,3 +30,6 @@
 [submodule "src/github.com/gorhill/cronexpr"]
        path = src/github.com/gorhill/cronexpr
        url = https://github.com/gorhill/cronexpr.git
+[submodule "src/cypherpunks.ru/balloon"]
+       path = src/cypherpunks.ru/balloon
+       url = git://git.cypherpunks.ru/balloon.git
index 02dee666cc7c33dbc14172b9dd984db03e75590b..327f8dfff34ca607685ca0e3db9e22fe4a2765bc 100644 (file)
--- a/common.mk
+++ b/common.mk
@@ -19,6 +19,7 @@ LDFLAGS = \
 ALL = \
        nncp-call \
        nncp-caller \
+       nncp-cfgenc \
        nncp-cfgmin \
        nncp-cfgnew \
        nncp-check \
@@ -42,6 +43,9 @@ nncp-call:
 nncp-caller:
        GOPATH=$(GOPATH) go build -ldflags "$(LDFLAGS)" cypherpunks.ru/nncp/cmd/nncp-caller
 
+nncp-cfgenc:
+       GOPATH=$(GOPATH) go build -ldflags "$(LDFLAGS)" cypherpunks.ru/nncp/cmd/nncp-cfgenc
+
 nncp-cfgmin:
        GOPATH=$(GOPATH) go build -ldflags "$(LDFLAGS)" cypherpunks.ru/nncp/cmd/nncp-cfgmin
 
index dc4b4efc09262714d297ad04d6798ef61098edb9..039fc2d1a079383f45339a777e46a113a7a42661 100644 (file)
@@ -5,8 +5,9 @@ Nearly all commands have the following common options:
 
 @table @option
 @item -cfg
-    Path to configuration file. May be overrided by @env{NNCPCFG}
-    environment variable.
+    Path to configuration file. May be overridden by @env{NNCPCFG}
+    environment variable. If file file is an encrypted @ref{EBlob,
+    eblob}, then ask for passphrase to decrypt it first.
 @item -debug
     Print debug messages. Normally this option should not be used.
 @item -minsize
@@ -82,6 +83,46 @@ file is renamed from @file{.part} one and when you rerun
 @command{nncp-call} again, remote node will receive completion
 notification.
 
+@node nncp-cfgenc
+@section nncp-cfgenc
+
+@verbatim
+% nncp-cfgmin [options] [-s INT] [-t INT] [-p INT] cfg.yaml > cfg.yaml.eblob
+% nncp-cfgmin [options] -d cfg.yaml.eblob > cfg.yaml
+@end verbatim
+
+This command allows you to encrypt provided @file{cfg.yaml} file with
+the passphrase, producing @ref{EBlob, eblob}, to safely keep your
+configuration file with private keys. This utility was written for users
+who do not want (or can not) to use either @url{https://gnupg.org/,
+GnuPG} or similar tools. That @file{eblob} file can be used directly in
+@option{-cfg} option of nearly all commands.
+
+@option{-s}, @option{-t}, @option{-p} are used to tune @file{eblob}'s
+password strengthening function. Space memory cost (@option{-s}),
+specified in number of BLAKE2b-256 blocks (32 bytes), tells how many
+memory must be used for hashing -- bigger values are better, but slower.
+Time cost (@option{-t}) tells how many rounds/iterations must be
+performed -- bigger is better, but slower. Number of parallel jobs
+(@option{-p}) tells how many computation processes will be run: this is
+the same as running that number of independent hashers and then joining
+their result together.
+
+When invoked for encryption, passphrase is entered manually twice. When
+invoked for decryption (@option{-d} option), it is asked once and exits
+if passphrase can not decrypt @file{eblob}.
+
+@option{-dump} options parses @file{eblob} and prints parameters used
+during its creation. For example:
+@verbatim
+% nncp-cfgenc -dump /usr/local/etc/nncp.yaml.eblob
+Strengthening function: Balloon with BLAKE2b-256
+Memory space cost: 1048576 bytes
+Number of rounds: 16
+Number of parallel jobs: 2
+Blob size: 2494
+@end verbatim
+
 @node nncp-cfgmin
 @section nncp-cfgmin
 
index dbb003c205d425f43fe5d9d6105a65e65fe8de07..0c683d564251b2e6eb72ac63e03d55b86b74068c 100644 (file)
@@ -9,6 +9,7 @@ Tarballs include all necessary required libraries:
 
 @multitable @columnfractions .50 .50
 @headitem Library @tab Licence
+@item @code{cypherpunks.ru/balloon} @tab GNU GPLv3+
 @item @code{github.com/dustin/go-humanize} @tab MIT
 @item @code{github.com/flynn/noise} @tab BSD 3-Clause
 @item @code{github.com/go-check/check} @tab BSD 2-Clause
diff --git a/doc/eblob.texi b/doc/eblob.texi
new file mode 100644 (file)
index 0000000..baaa718
--- /dev/null
@@ -0,0 +1,67 @@
+@node EBlob
+@unnumbered EBlob format
+
+Eblob is an encrypted blob (binary large object, in the terms of
+databases), holding any kind of symmetrically encrypted data with the
+passphrase used to derive the key. It is used to secure configuration
+files, holding valuable private keys, allowing them to be transferred
+safely everywhere.
+
+In fact it uses two factors for securing the data:
+
+@itemize
+@item @strong{salt}, that is kept inside @file{eblob}, something @emph{you have}
+@item @strong{passphrase}, that is kept inside the head, something @emph{you know}
+@end itemize
+
+Whole security depends on the passphrase itself. Pay attention that this
+is @strong{not} the password. Password is a short string of high entropy
+(highly random) characters, but passphrase is (very) long string of
+low-entropy characters. Low-entropy text is much more easier to
+remember, and its length provides pretty enough entropy as a result.
+
+Password strengthening function is applied to that passphrase to
+mitigate brute-force and dictionary attacks on it. Here,
+@url{https://crypto.stanford.edu/balloon/, Balloon} memory-hard password
+hashing function is used, together with BLAKE2b-256 hash. It has proven
+memory-hardness properties, very easy to implement, resistant to cache
+attacks and seems more secure than Argon2
+(@url{https://password-hashing.net/, Password Hashing Competition}
+winner).
+
+Eblob is an @url{https://tools.ietf.org/html/rfc4506, XDR}-encoded structure:
+
+@verbatim
++-------+------------------+------------+
+| MAGIC | S | T | P | SALT | BLOB | MAC |
++-------+------------------+------------+
+@end verbatim
+
+@multitable @columnfractions 0.2 0.3 0.5
+@headitem @tab XDR type @tab Value
+@item Magic number @tab
+    8-byte, fixed length opaque data @tab
+    @verb{|N N C P B 0x00 0x00 0x01|}
+@item S, T, P @tab
+    unsigned integer @tab
+    Space cost, time cost and parallel jobs number
+@item Salt @tab
+    32 bytes, fixed length opaque data @tab
+    Randomly generated salt
+@item Blob @tab
+    variable length opaque data @tab
+    Encrypted data itself
+@item MAC @tab
+    32 bytes, fixed length opaque data @tab
+    BLAKE2b-256 MAC of encrypted blob
+@end multitable
+
+Blob's encryption is done using
+@url{https://www.schneier.com/academic/twofish/, Twofish} algorithm with
+256-bit key in
+@url{https://en.wikipedia.org/wiki/Counter_mode#Counter_.28CTR.29, CTR}
+mode of operation with zero initialization vector.
+@code{balloon(BLAKE2b-256, S, T, P, salt, password)} gives the main key,
+that is fed to @url{https://en.wikipedia.org/wiki/HKDF,
+HKDF}-BLAKE2b-256 KDF. Actual encryption key for Twofish and
+authentication key for MAC are derived from that KDF.
index 364fc8cfd9af01d9f7fc8d994b53d4093efb337b..87c008e6d304d0dbcabce27404109e34cd293549 100644 (file)
@@ -41,6 +41,7 @@ A copy of the license is included in the section entitled "Copying conditions".
 * Log format: Log.
 * Packet format: Packet.
 * Sync protocol: Sync.
+* EBlob format: EBlob.
 * Thanks::
 * Contacts and feedback: Contacts.
 * Copying conditions: Copying.
@@ -62,6 +63,7 @@ A copy of the license is included in the section entitled "Copying conditions".
 @include log.texi
 @include pkt.texi
 @include sp.texi
+@include eblob.texi
 @include thanks.texi
 @include contacts.texi
 
index dccde596a9c4f5eee3fbaafad0f81be8efcf86e9..2f6bc2e9d262b774ca03d05a7740e1b20520a753 100644 (file)
@@ -13,6 +13,7 @@
 @command{nncp-reass} командой и @option{freqchunked} опцией
 конфигурационного файла. Полезно для передачи больших файлов через
 маленькие устройства хранения.
+
 @item
 @option{freqminsize} опция конфигурационного файла, аналогичная
 @option{-minsize}.
 а @command{nncp-mincfg} в @command{nncp-cfgmin}, для того чтобы они
 имели общий префикс и были сгруппированы для удобства.
 
+@item Появилась команда @command{nncp-cfgenc}, позволяющая
+шифровать/дешифровать конфигурационный файл, чтобы безопасно его хранить
+без использования OpenPGP или других подобных инструментов.
+
 @item
 Обновлены зависимые криптографические библиотеки.
 @end itemize
index 5dec744f224a7ec295c2062df9a46f283af9f9bc..7bd287ce0ec758d7007e3edeb687f27d1903d873 100644 (file)
@@ -33,14 +33,19 @@ and @command{nncp-mincfg} to @command{nncp-cfgmin} -- now they have
 common prefix and are grouped together for convenience.
 
 @item
-Cryptographic libraries (dependecies) are updated.
+@command{nncp-cfgenc} command appeared, allowing configuration file
+encryption/decryption, for keeping it safe without any either OpenPGP or
+similar tools usage.
+
+@item
+Cryptographic libraries (dependencies) are updated.
 @end itemize
 
 @node Release 0.6
 @section Release 0.6
 @itemize
 @item Small @command{nncp-rm} command appeared.
-@item Cryptographic libraries (dependecies) are updated.
+@item Cryptographic libraries (dependencies) are updated.
 @end itemize
 
 @node Release 0.5
index 5ec2c736dae95a35768990851d57d86777d3ee1b..137e7f9ec73704f30c5e13fd5004241b2e73e076 100644 (file)
@@ -5,7 +5,7 @@ NNCP consists of several utilities. As a rule you will have the
 following workflow:
 
 @enumerate
-@item Run @ref{nncp-newcfg} on each node to create an initial
+@item Run @ref{nncp-cfgnew} on each node to create an initial
 @ref{Configuration, configuration} file.
 @item Tune it up and set at least @ref{Spool, spool} and log paths.
 @item Share your public keys and reachability addressees with your
index aeed4e157c685b4224e116af2afa8eaf1472a599..a68f4365515873561c4e2247854ca798b836df77 100755 (executable)
@@ -39,6 +39,7 @@ golang.org/x/crypto/hkdf
 golang.org/x/crypto/nacl
 golang.org/x/crypto/poly1305
 golang.org/x/crypto/salsa20
+golang.org/x/crypto/ssh/terminal
 golang.org/x/crypto/twofish
 golang.org/x/net/AUTHORS
 golang.org/x/net/CONTRIBUTORS
index 7e2fcb694a861d0a5dd4bee6d7505e0cb6c23d30..3738689a2fe30c54fb2f90758a944504f5520ae7 100644 (file)
@@ -28,6 +28,7 @@ INSTALL_TARGET=       install-strip
 
 PLIST_FILES=   bin/nncp-call \
                bin/nncp-caller \
+               bin/nncp-cfgenc \
                bin/nncp-cfgmin \
                bin/nncp-cfgnew \
                bin/nncp-check \
diff --git a/src/cypherpunks.ru/balloon b/src/cypherpunks.ru/balloon
new file mode 160000 (submodule)
index 0000000..2be0740
--- /dev/null
@@ -0,0 +1 @@
+Subproject commit 2be074075c635f95406490655039988c8e3633d8
index 9419886071a5ac657729825ef3f428cfb1f1a9aa..3400ed83a93e7d850f85da7b47653497592e14a9 100644 (file)
@@ -19,12 +19,15 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 package nncp
 
 import (
+       "bytes"
        "errors"
+       "log"
        "os"
        "path"
 
        "github.com/gorhill/cronexpr"
        "golang.org/x/crypto/ed25519"
+       "golang.org/x/crypto/ssh/terminal"
        "gopkg.in/yaml.v2"
 )
 
@@ -334,9 +337,21 @@ func (nodeOur *NodeOur) ToYAML() string {
 }
 
 func CfgParse(data []byte) (*Ctx, error) {
+       var err error
+       if bytes.Compare(data[:8], MagicNNCPBv1[:]) == 0 {
+               os.Stderr.WriteString("Passphrase:")
+               password, err := terminal.ReadPassword(0)
+               if err != nil {
+                       log.Fatalln(err)
+               }
+               os.Stderr.WriteString("\n")
+               data, err = DeEBlob(data, password)
+               if err != nil {
+                       return nil, err
+               }
+       }
        var cfgYAML CfgYAML
-       err := yaml.Unmarshal(data, &cfgYAML)
-       if err != nil {
+       if err = yaml.Unmarshal(data, &cfgYAML); err != nil {
                return nil, err
        }
        if _, exists := cfgYAML.Neigh["self"]; !exists {
diff --git a/src/cypherpunks.ru/nncp/cmd/nncp-cfgenc/main.go b/src/cypherpunks.ru/nncp/cmd/nncp-cfgenc/main.go
new file mode 100644 (file)
index 0000000..9376d33
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+NNCP -- Node to Node copy, utilities for store-and-forward data exchange
+Copyright (C) 2016-2017 Sergey Matveev <stargrave@stargrave.org>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+// NNCP configuration file encrypter/decrypter.
+package main
+
+import (
+       "bytes"
+       "errors"
+       "flag"
+       "fmt"
+       "io/ioutil"
+       "log"
+       "os"
+
+       "cypherpunks.ru/nncp"
+       "github.com/davecgh/go-xdr/xdr2"
+       "golang.org/x/crypto/blake2b"
+       "golang.org/x/crypto/ssh/terminal"
+)
+
+func usage() {
+       fmt.Fprintf(os.Stderr, nncp.UsageHeader())
+       fmt.Fprintln(os.Stderr, "nncp-cfgenc -- encrypt/decrypt configuration file\n")
+       fmt.Fprintf(os.Stderr, "Usage: %s [options] cfg.yaml > cfg.yaml.eblob\n", os.Args[0])
+       fmt.Fprintf(os.Stderr, "       %s [options] -d cfg.yaml.eblob > cfg.yaml\n", os.Args[0])
+       fmt.Fprintf(os.Stderr, "       %s [options] -dump cfg.yaml.eblob\n", os.Args[0])
+       fmt.Fprintln(os.Stderr, "Options:")
+       flag.PrintDefaults()
+}
+
+func main() {
+       var (
+               decrypt  = flag.Bool("d", false, "Decrypt the file")
+               dump     = flag.Bool("dump", false, "Print human-readable eblob information")
+               sOpt     = flag.Int("s", nncp.DefaultS, "Balloon space cost, in 32 bytes chunks")
+               tOpt     = flag.Int("t", nncp.DefaultT, "Balloon time cost, number of rounds")
+               pOpt     = flag.Int("p", nncp.DefaultP, "Balloon number of parallel jobs")
+               version  = flag.Bool("version", false, "Print version information")
+               warranty = flag.Bool("warranty", false, "Print warranty information")
+       )
+       flag.Usage = usage
+       flag.Parse()
+       if *warranty {
+               fmt.Println(nncp.Warranty)
+               return
+       }
+       if *version {
+               fmt.Println(nncp.VersionGet())
+               return
+       }
+
+       if flag.NArg() != 1 {
+               usage()
+               os.Exit(1)
+       }
+
+       data, err := ioutil.ReadFile(flag.Arg(0))
+       if err != nil {
+               log.Fatalln("Can not read data:", err)
+       }
+       if *dump {
+               var eblob nncp.EBlob
+               if _, err := xdr.Unmarshal(bytes.NewReader(data), &eblob); err != nil {
+                       log.Fatalln(err)
+               }
+               if eblob.Magic != nncp.MagicNNCPBv1 {
+                       log.Fatalln(errors.New("Unknown eblob type"))
+               }
+               fmt.Println("Strengthening function: Balloon with BLAKE2b-256")
+               fmt.Printf("Memory space cost: %d bytes\n", eblob.SCost*blake2b.Size256)
+               fmt.Printf("Number of rounds: %d\n", eblob.TCost)
+               fmt.Printf("Number of parallel jobs: %d\n", eblob.PCost)
+               fmt.Printf("Blob size: %d\n", len(eblob.Blob))
+               os.Exit(0)
+       }
+       if *decrypt {
+               os.Stderr.WriteString("Passphrase:")
+               password, err := terminal.ReadPassword(0)
+               if err != nil {
+                       log.Fatalln(err)
+               }
+               os.Stderr.WriteString("\n")
+               cfgRaw, err := nncp.DeEBlob(data, password)
+               if err != nil {
+                       log.Fatalln(err)
+               }
+               os.Stdout.Write(cfgRaw)
+       } else {
+               os.Stderr.WriteString("Passphrase:")
+               password1, err := terminal.ReadPassword(0)
+               if err != nil {
+                       log.Fatalln(err)
+               }
+               os.Stderr.WriteString("\n")
+               os.Stderr.WriteString("Repeat passphrase:")
+               password2, err := terminal.ReadPassword(0)
+               if err != nil {
+                       log.Fatalln(err)
+               }
+               os.Stderr.WriteString("\n")
+               if bytes.Compare(password1, password2) != 0 {
+                       log.Fatalln(errors.New("Passphrases do not match"))
+               }
+               eblob, err := nncp.NewEBlob(*sOpt, *tOpt, *pOpt, password1, data)
+               if err != nil {
+                       log.Fatalln(err)
+               }
+               os.Stdout.Write(eblob)
+       }
+}
diff --git a/src/cypherpunks.ru/nncp/eblob.go b/src/cypherpunks.ru/nncp/eblob.go
new file mode 100644 (file)
index 0000000..cb241ab
--- /dev/null
@@ -0,0 +1,153 @@
+/*
+NNCP -- Node to Node copy, utilities for store-and-forward data exchange
+Copyright (C) 2016-2017 Sergey Matveev <stargrave@stargrave.org>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package nncp
+
+import (
+       "bytes"
+       "crypto/cipher"
+       "crypto/rand"
+       "crypto/subtle"
+       "errors"
+       "io"
+
+       "cypherpunks.ru/balloon"
+       "github.com/davecgh/go-xdr/xdr2"
+       "golang.org/x/crypto/blake2b"
+       "golang.org/x/crypto/hkdf"
+       "golang.org/x/crypto/twofish"
+)
+
+const (
+       DefaultS = 1 << 20 / 32
+       DefaultT = 1 << 4
+       DefaultP = 2
+)
+
+var (
+       MagicNNCPBv1 [8]byte = [8]byte{'N', 'N', 'C', 'P', 'B', 0, 0, 1}
+)
+
+type EBlob struct {
+       Magic [8]byte
+       SCost uint32
+       TCost uint32
+       PCost uint32
+       Salt  *[32]byte
+       Blob  []byte
+       MAC   *[blake2b.Size256]byte
+}
+
+// Create an encrypted blob. sCost -- memory space requirements, number
+// of hash-output sized (32 bytes) blocks. tCost -- time requirements,
+// number of rounds. pCost -- number of parallel jobs.
+func NewEBlob(sCost, tCost, pCost int, password, data []byte) ([]byte, error) {
+       salt := new([32]byte)
+       var err error
+       if _, err = rand.Read(salt[:]); err != nil {
+               return nil, err
+       }
+       key := balloon.H(blake256, password, salt[:], sCost, tCost, pCost)
+       kdf := hkdf.New(blake256, key, nil, MagicNNCPBv1[:])
+       keyEnc := make([]byte, 32)
+       if _, err = io.ReadFull(kdf, keyEnc); err != nil {
+               return nil, err
+       }
+       keyAuth := make([]byte, 64)
+       if _, err = io.ReadFull(kdf, keyAuth); err != nil {
+               return nil, err
+       }
+       ciph, err := twofish.NewCipher(keyEnc)
+       if err != nil {
+               return nil, err
+       }
+       ctr := cipher.NewCTR(ciph, make([]byte, twofish.BlockSize))
+       mac, err := blake2b.New256(keyAuth)
+       if err != nil {
+               return nil, err
+       }
+       var blob bytes.Buffer
+       mw := io.MultiWriter(&blob, mac)
+       ae := &cipher.StreamWriter{S: ctr, W: mw}
+       if _, err = ae.Write(data); err != nil {
+               return nil, err
+       }
+       macTag := new([blake2b.Size256]byte)
+       mac.Sum(macTag[:0])
+       eblob := EBlob{
+               Magic: MagicNNCPBv1,
+               SCost: uint32(sCost),
+               TCost: uint32(tCost),
+               PCost: uint32(pCost),
+               Salt:  salt,
+               Blob:  blob.Bytes(),
+               MAC:   macTag,
+       }
+       var eblobRaw bytes.Buffer
+       if _, err = xdr.Marshal(&eblobRaw, &eblob); err != nil {
+               return nil, err
+       }
+       return eblobRaw.Bytes(), nil
+}
+
+func DeEBlob(eblobRaw, password []byte) ([]byte, error) {
+       var eblob EBlob
+       var err error
+       if _, err = xdr.Unmarshal(bytes.NewReader(eblobRaw), &eblob); err != nil {
+               return nil, err
+       }
+       if eblob.Magic != MagicNNCPBv1 {
+               return nil, BadMagic
+       }
+       key := balloon.H(
+               blake256,
+               password,
+               eblob.Salt[:],
+               int(eblob.SCost),
+               int(eblob.TCost),
+               int(eblob.PCost),
+       )
+       kdf := hkdf.New(blake256, key, nil, MagicNNCPBv1[:])
+       keyEnc := make([]byte, 32)
+       if _, err = io.ReadFull(kdf, keyEnc); err != nil {
+               return nil, err
+       }
+       keyAuth := make([]byte, 64)
+       if _, err = io.ReadFull(kdf, keyAuth); err != nil {
+               return nil, err
+       }
+       ciph, err := twofish.NewCipher(keyEnc)
+       if err != nil {
+               return nil, err
+       }
+       ctr := cipher.NewCTR(ciph, make([]byte, twofish.BlockSize))
+       mac, err := blake2b.New256(keyAuth)
+       if err != nil {
+               return nil, err
+       }
+       var blob bytes.Buffer
+       tr := io.TeeReader(bytes.NewReader(eblob.Blob), mac)
+       ae := &cipher.StreamReader{S: ctr, R: tr}
+       if _, err = io.Copy(&blob, ae); err != nil {
+               return nil, err
+       }
+       if subtle.ConstantTimeCompare(mac.Sum(nil), eblob.MAC[:]) != 1 {
+               return nil, errors.New("Unauthenticated blob")
+       }
+       return blob.Bytes(), nil
+}