|  | // Copyright 2020 The Monogon Project Authors. | 
|  | // | 
|  | // SPDX-License-Identifier: Apache-2.0 | 
|  | // | 
|  | // Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | // you may not use this file except in compliance with the License. | 
|  | // You may obtain a copy of the License at | 
|  | // | 
|  | //     http://www.apache.org/licenses/LICENSE-2.0 | 
|  | // | 
|  | // Unless required by applicable law or agreed to in writing, software | 
|  | // distributed under the License is distributed on an "AS IS" BASIS, | 
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | // See the License for the specific language governing permissions and | 
|  | // limitations under the License. | 
|  |  | 
|  | // This package implements the minimum of functionality needed to generate and | 
|  | // map dm-verity images. It's provided in order to avoid a perceived higher | 
|  | // long term cost of packaging, linking against and maintaining the original C | 
|  | // veritysetup tool. | 
|  | // | 
|  | // dm-verity is a Linux device mapper target that allows integrity verification of | 
|  | // a read-only block device. The block device whose integrity should be checked | 
|  | // (the 'data device') must be first processed by a tool like veritysetup to | 
|  | // generate a hash device and root hash. | 
|  | // The original data device, hash device and root hash are then set up as a device | 
|  | // mapper target, and any read performed from the data device through the verity | 
|  | // target will be verified for integrity by Linux using the hash device and root | 
|  | // hash. | 
|  | // | 
|  | // Internally, the hash device is a Merkle tree of all the bytes in the data | 
|  | // device, layed out as layers of 'hash blocks'. Starting with data bytes, layers | 
|  | // are built recursively, with each layer's output hash blocks becoming the next | 
|  | // layer's data input, ending with the single root hash. | 
|  | // | 
|  | // For more information about the internals, see the Linux and cryptsetup | 
|  | // upstream code: | 
|  | // | 
|  | // https://gitlab.com/cryptsetup/cryptsetup/wikis/DMVerity | 
|  | package verity | 
|  |  | 
|  | import ( | 
|  | "bytes" | 
|  | "crypto/rand" | 
|  | "crypto/sha256" | 
|  | "encoding/binary" | 
|  | "encoding/hex" | 
|  | "fmt" | 
|  | "io" | 
|  | "strconv" | 
|  | "strings" | 
|  | ) | 
|  |  | 
|  | // superblock represents data layout inside of a dm-verity hash block | 
|  | // device superblock. It follows a preexisting verity implementation: | 
|  | // | 
|  | // https://gitlab.com/cryptsetup/cryptsetup/wikis/DMVerity#verity-superblock-format | 
|  | type superblock struct { | 
|  | // signature is the magic signature of a verity hash device superblock, | 
|  | // "verity\0\0". | 
|  | signature [8]byte | 
|  | // version specifies a superblock format. This structure describes version | 
|  | // '1'. | 
|  | version uint32 | 
|  | // hashType defaults to '1' outside Chrome OS, according to scarce dm-verity | 
|  | // documentation. | 
|  | hashType uint32 | 
|  | // uuid contains a UUID of the hash device. | 
|  | uuid [16]byte | 
|  | // algorithm stores an ASCII-encoded name of the hash function used. | 
|  | algorithm [32]byte | 
|  |  | 
|  | // dataBlockSize specifies a size of a single data device block, in bytes. | 
|  | dataBlockSize uint32 | 
|  | // hashBlockSize specifies a size of a single hash device block, in bytes. | 
|  | hashBlockSize uint32 | 
|  | // dataBlocks contains a count of blocks available on the data device. | 
|  | dataBlocks uint64 | 
|  |  | 
|  | // saltSize encodes the size of hash block salt, up to the maximum of 256 bytes. | 
|  | saltSize uint16 | 
|  |  | 
|  | // padding | 
|  | _ [6]byte | 
|  | // exactly saltSize bytes of salt are prepended to data blocks before hashing. | 
|  | saltBuffer [256]byte | 
|  | // padding | 
|  | _ [168]byte | 
|  | } | 
|  |  | 
|  | // newSuperblock builds a dm-verity hash device superblock based on | 
|  | // hardcoded defaults. dataBlocks is the only field left for later | 
|  | // initialization. | 
|  | // It returns either a partially initialized superblock, or an error. | 
|  | func newSuperblock() (*superblock, error) { | 
|  | // This implementation only handles SHA256-based verity hash images | 
|  | // with a specific 4096-byte block size. | 
|  | // Block sizes can be updated by adjusting the struct literal below. | 
|  | // A change of a hashing algorithm would require a refactor of | 
|  | // saltedDigest, and references to sha256.Size. | 
|  | // | 
|  | // Fill in the defaults (compare with superblock definition). | 
|  | sb := superblock{ | 
|  | signature:     [8]byte{'v', 'e', 'r', 'i', 't', 'y', 0, 0}, | 
|  | version:       1, | 
|  | hashType:      1, | 
|  | algorithm:     [32]byte{'s', 'h', 'a', '2', '5', '6'}, | 
|  | saltSize:      64, | 
|  | dataBlockSize: 4096, | 
|  | hashBlockSize: 4096, | 
|  | } | 
|  |  | 
|  | // Fill in the superblock UUID and cryptographic salt. | 
|  | if _, err := rand.Read(sb.uuid[:]); err != nil { | 
|  | return nil, fmt.Errorf("when generating UUID: %w", err) | 
|  | } | 
|  | if _, err := rand.Read(sb.saltBuffer[:]); err != nil { | 
|  | return nil, fmt.Errorf("when generating salt: %w", err) | 
|  | } | 
|  |  | 
|  | return &sb, nil | 
|  | } | 
|  |  | 
|  | // salt returns a slice of sb.saltBuffer actually occupied by | 
|  | // salt bytes, of sb.saltSize length. | 
|  | func (sb *superblock) salt() []byte { | 
|  | return sb.saltBuffer[:int(sb.saltSize)] | 
|  | } | 
|  |  | 
|  | // algorithmName returns a name of the algorithm used to hash data block | 
|  | // digests. | 
|  | func (sb *superblock) algorithmName() string { | 
|  | size := bytes.IndexByte(sb.algorithm[:], 0x00) | 
|  | return string(sb.algorithm[:size]) | 
|  | } | 
|  |  | 
|  | // saltedDigest computes and returns a SHA256 sum of a block prepended | 
|  | // with a Superblock-defined salt. | 
|  | func (sb *superblock) saltedDigest(data []byte) (digest [sha256.Size]byte) { | 
|  | h := sha256.New() | 
|  | h.Write(sb.salt()) | 
|  | h.Write(data) | 
|  | copy(digest[:], h.Sum(nil)) | 
|  | return | 
|  | } | 
|  |  | 
|  | // dataBlocksPerHashBlock returns the count of hash operation outputs that | 
|  | // fit in a hash device block. This is also the amount of data device | 
|  | // blocks it takes to populate a hash device block. | 
|  | func (sb *superblock) dataBlocksPerHashBlock() uint64 { | 
|  | return uint64(sb.hashBlockSize) / sha256.Size | 
|  | } | 
|  |  | 
|  | // computeHashBlock reads at most sb.dataBlocksPerHashBlock blocks from | 
|  | // the given reader object, returning a padded hash block of length | 
|  | // defined by sb.hashBlockSize, the count of digests output, and an | 
|  | // error, if encountered. | 
|  | // In case a non-nil block is returned, it's guaranteed to contain at | 
|  | // least one hash. An io.EOF signals that there is no more to be read. | 
|  | func (sb *superblock) computeHashBlock(r io.Reader) ([]byte, uint64, error) { | 
|  | // dcnt stores the total count of data blocks processed, which is the | 
|  | // as the count of digests output. | 
|  | var dcnt uint64 | 
|  | // Preallocate a whole hash block. | 
|  | hblk := bytes.NewBuffer(make([]byte, 0, sb.hashBlockSize)) | 
|  |  | 
|  | // For every data block, compute a hash and place it in hblk. Continue | 
|  | // till EOF. | 
|  | for b := uint64(0); b < sb.dataBlocksPerHashBlock(); b++ { | 
|  | dbuf := make([]byte, sb.dataBlockSize) | 
|  | // Attempt to read enough data blocks to make a complete hash block. | 
|  | n, err := io.ReadFull(r, dbuf) | 
|  | // If any data was read, make a hash and add it to the hash buffer. | 
|  | if n != 0 { | 
|  | hash := sb.saltedDigest(dbuf) | 
|  | hblk.Write(hash[:]) | 
|  | dcnt++ | 
|  | } | 
|  | // Handle the read errors. | 
|  | switch err { | 
|  | case nil: | 
|  | case io.ErrUnexpectedEOF, io.EOF: | 
|  | // io.ReadFull returns io.ErrUnexpectedEOF after a partial read, | 
|  | // and io.EOF if no bytes were read. In both cases it's possible | 
|  | // to end up with a partially filled hash block. | 
|  | if hblk.Len() != 0 { | 
|  | // Return a zero-padded hash block if any hashes were written | 
|  | // to it, and signal that no more blocks can be built. | 
|  | res := hblk.Bytes() | 
|  | return res[:cap(res)], dcnt, io.EOF | 
|  | } | 
|  | // Return nil if the block doesn't contain any hashes. | 
|  | return nil, 0, io.EOF | 
|  | default: | 
|  | // Wrap unhandled read errors. | 
|  | return nil, 0, fmt.Errorf("while computing a hash block: %w", err) | 
|  | } | 
|  | } | 
|  | // Return a completely filled hash block. | 
|  | res := hblk.Bytes() | 
|  | return res[:cap(res)], dcnt, nil | 
|  | } | 
|  |  | 
|  | // WriteTo writes a verity superblock to a given writer object. | 
|  | // It returns the count of bytes written, and a write error, if | 
|  | // encountered. | 
|  | func (sb *superblock) WriteTo(w io.Writer) (int64, error) { | 
|  | // Write the superblock. | 
|  | if err := binary.Write(w, binary.LittleEndian, sb); err != nil { | 
|  | return -1, fmt.Errorf("while writing a header: %w", err) | 
|  | } | 
|  |  | 
|  | // Get the padding size by substracting current offset from a hash block | 
|  | // size. | 
|  | co := int(binary.Size(sb)) | 
|  | pbc := int(sb.hashBlockSize) - int(co) | 
|  | if pbc <= 0 { | 
|  | return int64(co), fmt.Errorf("hash device block size smaller than dm-verity superblock") | 
|  | } | 
|  |  | 
|  | // Write the padding bytes at the end of the block. | 
|  | n, err := w.Write(bytes.Repeat([]byte{0}, pbc)) | 
|  | co += n | 
|  | if err != nil { | 
|  | return int64(co), fmt.Errorf("while writing padding: %w", err) | 
|  | } | 
|  | return int64(co), nil | 
|  | } | 
|  |  | 
|  | // computeLevel produces a verity hash tree level based on data read from | 
|  | // a given reader object. | 
|  | // It returns a byte slice containing one or more hash blocks, or an | 
|  | // error. | 
|  | // BUG(mz): Current implementation requires a 1/128th of the data image | 
|  | // size to be allocatable on the heap. | 
|  | func (sb *superblock) computeLevel(r io.Reader) ([]byte, error) { | 
|  | // hbuf will store all the computed hash blocks. | 
|  | var hbuf bytes.Buffer | 
|  | // Compute one or more hash blocks, reading all data available in the | 
|  | // 'r' reader object, and write them into hbuf. | 
|  | for { | 
|  | hblk, _, err := sb.computeHashBlock(r) | 
|  | if err != nil && err != io.EOF { | 
|  | return nil, fmt.Errorf("while building a hash tree level: %w", err) | 
|  | } | 
|  | if hblk != nil { | 
|  | _, err := hbuf.Write(hblk) | 
|  | if err != nil { | 
|  | return nil, fmt.Errorf("while writing to hash block buffer: %w", err) | 
|  | } | 
|  | } | 
|  | if err == io.EOF { | 
|  | break | 
|  | } | 
|  | } | 
|  | return hbuf.Bytes(), nil | 
|  | } | 
|  |  | 
|  | // hashTree stores hash tree levels, each level comprising one or more | 
|  | // Verity hash blocks. Levels are ordered from bottom to top. | 
|  | type hashTree [][]byte | 
|  |  | 
|  | // push appends a level to the hash tree. | 
|  | func (ht *hashTree) push(nl []byte) { | 
|  | *ht = append(*ht, nl) | 
|  | } | 
|  |  | 
|  | // top returns the topmost level of the hash tree. | 
|  | func (ht *hashTree) top() []byte { | 
|  | if len(*ht) == 0 { | 
|  | return nil | 
|  | } | 
|  | return (*ht)[len(*ht)-1] | 
|  | } | 
|  |  | 
|  | // WriteTo writes a verity-formatted hash tree to the given writer | 
|  | // object. | 
|  | // It returns a write error, if encountered. | 
|  | func (ht *hashTree) WriteTo(w io.Writer) (int64, error) { | 
|  | // t keeps the count of bytes written to w. | 
|  | var t int64 | 
|  | // Write the hash tree levels from top to bottom. | 
|  | for l := len(*ht) - 1; l >= 0; l-- { | 
|  | level := (*ht)[l] | 
|  | // Call w.Write until a whole level is written. | 
|  | for len(level) != 0 { | 
|  | n, err := w.Write(level) | 
|  | if err != nil { | 
|  | return t, fmt.Errorf("while writing a level: %w", err) | 
|  | } | 
|  | level = level[n:] | 
|  | t += int64(n) | 
|  | } | 
|  | } | 
|  | return t, nil | 
|  | } | 
|  |  | 
|  | // MappingTable aggregates data needed to generate a complete Verity | 
|  | // mapping table. | 
|  | type MappingTable struct { | 
|  | // superblock defines the following elements of the mapping table: | 
|  | // - data device block size | 
|  | // - hash device block size | 
|  | // - total count of data blocks | 
|  | // - hash algorithm used | 
|  | // - cryptographic salt used | 
|  | superblock *superblock | 
|  | // DataDevicePath is the filesystem path of the data device used as part | 
|  | // of the Verity Device Mapper target. | 
|  | DataDevicePath string | 
|  | // HashDevicePath is the filesystem path of the hash device used as part | 
|  | // of the Verity Device Mapper target. | 
|  | HashDevicePath string | 
|  | // HashStart marks the starting block of the Verity hash tree. | 
|  | HashStart int64 | 
|  | // rootHash stores a cryptographic hash of the top hash tree block. | 
|  | rootHash []byte | 
|  | } | 
|  |  | 
|  | // VerityParameterList returns a list of Verity target parameters, ordered | 
|  | // as they would appear in a parameter string. | 
|  | func (t *MappingTable) VerityParameterList() []string { | 
|  | return []string{ | 
|  | "1", | 
|  | t.DataDevicePath, | 
|  | t.HashDevicePath, | 
|  | strconv.FormatUint(uint64(t.superblock.dataBlockSize), 10), | 
|  | strconv.FormatUint(uint64(t.superblock.hashBlockSize), 10), | 
|  | strconv.FormatUint(uint64(t.superblock.dataBlocks), 10), | 
|  | strconv.FormatInt(int64(t.HashStart), 10), | 
|  | t.superblock.algorithmName(), | 
|  | hex.EncodeToString(t.rootHash), | 
|  | hex.EncodeToString(t.superblock.salt()), | 
|  | } | 
|  | } | 
|  |  | 
|  | // TargetParameters returns the mapping table as a list of Device Mapper | 
|  | // target parameters, ordered as they would appear in a parameter string | 
|  | // (see: String). | 
|  | func (t *MappingTable) TargetParameters() []string { | 
|  | return append( | 
|  | []string{ | 
|  | "0", | 
|  | strconv.FormatUint(t.Length(), 10), | 
|  | "verity", | 
|  | }, | 
|  | t.VerityParameterList()..., | 
|  | ) | 
|  | } | 
|  |  | 
|  | // String returns a string-formatted mapping table for use with Device | 
|  | // Mapper. | 
|  | // BUG(mz): unescaped whitespace can appear in block device paths | 
|  | func (t *MappingTable) String() string { | 
|  | return strings.Join(t.TargetParameters(), " ") | 
|  | } | 
|  |  | 
|  | // Length returns the data device length, represented as a number of | 
|  | // 512-byte sectors. | 
|  | func (t *MappingTable) Length() uint64 { | 
|  | return t.superblock.dataBlocks * uint64(t.superblock.dataBlockSize) / 512 | 
|  | } | 
|  |  | 
|  | // encoder transforms data blocks written into it into a verity hash | 
|  | // tree. It writes out the hash tree only after Close is called on it. | 
|  | type encoder struct { | 
|  | // out is the writer object Encoder will write to. | 
|  | out io.Writer | 
|  | // writeSb, if true, will cause a Verity superblock to be written to the | 
|  | // writer object. | 
|  | writeSb bool | 
|  | // sb contains the most of information needed to build a mapping table. | 
|  | sb *superblock | 
|  | // bottom stands for the bottom level of the hash tree. It contains | 
|  | // complete hash blocks of data written to the encoder. | 
|  | bottom bytes.Buffer | 
|  | // dataBuffer stores incoming data for later processing. | 
|  | dataBuffer bytes.Buffer | 
|  | // rootHash stores the verity root hash set on Close. | 
|  | rootHash []byte | 
|  | } | 
|  |  | 
|  | // computeHashTree builds a complete hash tree based on the encoder's | 
|  | // state. Levels are appended to the returned hash tree starting from the | 
|  | // bottom, with the top level written last. | 
|  | // e.sb.dataBlocks is set according to the bottom level's length, which | 
|  | // must be divisible by e.sb.hashBlockSize. | 
|  | // e.rootHash is set on success. | 
|  | // It returns an error, if encountered. | 
|  | func (e *encoder) computeHashTree() (*hashTree, error) { | 
|  | // Put b at the bottom of the tree. Don't perform a deep copy. | 
|  | ht := hashTree{e.bottom.Bytes()} | 
|  |  | 
|  | // Other levels are built by hashing the hash blocks comprising a level | 
|  | // below. | 
|  | for { | 
|  | if len(ht.top()) == int(e.sb.hashBlockSize) { | 
|  | // The last level to compute has a size of exactly one hash block. | 
|  | // That's the root level. Its hash serves as a cryptographic root of | 
|  | // trust and is saved into a encoder for later use. | 
|  | // In case the bottom level consists of only one hash block, no more | 
|  | // levels are computed. | 
|  | sd := e.sb.saltedDigest(ht.top()) | 
|  | e.rootHash = sd[:] | 
|  | return &ht, nil | 
|  | } | 
|  |  | 
|  | // Create the next level by hashing the previous one. | 
|  | nl, err := e.sb.computeLevel(bytes.NewReader(ht.top())) | 
|  | if err != nil { | 
|  | return nil, fmt.Errorf("while computing a level: %w", err) | 
|  | } | 
|  | // Append the resulting next level to a tree. | 
|  | ht.push(nl) | 
|  | } | 
|  | } | 
|  |  | 
|  | // processDataBuffer processes data blocks contained in e.dataBuffer | 
|  | // until no more data is available to form a completely filled hash block. | 
|  | // If 'incomplete' is true, all remaining data in e.dataBuffer will be | 
|  | // processed, producing a terminating incomplete block. | 
|  | // It returns the count of data blocks processed, or an error, if | 
|  | // encountered. | 
|  | func (e *encoder) processDataBuffer(incomplete bool) (uint64, error) { | 
|  | // tdcnt stores the total count of data blocks processed. | 
|  | var tdcnt uint64 | 
|  | // Compute the count of bytes needed to produce a complete hash block. | 
|  | bph := e.sb.dataBlocksPerHashBlock() * uint64(e.sb.dataBlockSize) | 
|  |  | 
|  | // Iterate until no more data is available in e.dbuf. | 
|  | for uint64(e.dataBuffer.Len()) >= bph || incomplete && e.dataBuffer.Len() != 0 { | 
|  | hb, dcnt, err := e.sb.computeHashBlock(&e.dataBuffer) | 
|  | if err != nil && err != io.EOF { | 
|  | return 0, fmt.Errorf("while processing a data buffer: %w", err) | 
|  | } | 
|  | // Increment the total count of data blocks processed. | 
|  | tdcnt += dcnt | 
|  | // Write the resulting hash block into the level-zero buffer. | 
|  | e.bottom.Write(hb[:]) | 
|  | } | 
|  | return tdcnt, nil | 
|  | } | 
|  |  | 
|  | // NewEncoder returns a fully initialized encoder, or an error. The | 
|  | // encoder will write to the given io.Writer object. | 
|  | // A verity superblock will be written, preceding the hash tree, if | 
|  | // writeSb is true. | 
|  | func NewEncoder(out io.Writer, dataBlockSize, hashBlockSize uint32, writeSb bool) (*encoder, error) { | 
|  | sb, err := newSuperblock() | 
|  | if err != nil { | 
|  | return nil, fmt.Errorf("while creating a superblock: %w", err) | 
|  | } | 
|  | sb.dataBlockSize = dataBlockSize | 
|  | sb.hashBlockSize = hashBlockSize | 
|  |  | 
|  | e := encoder{ | 
|  | out:     out, | 
|  | writeSb: writeSb, | 
|  | sb:      sb, | 
|  | } | 
|  | return &e, nil | 
|  | } | 
|  |  | 
|  | // Write hashes raw data to form the bottom hash tree level. | 
|  | // It returns the number of bytes written, and an error, if encountered. | 
|  | func (e *encoder) Write(data []byte) (int, error) { | 
|  | // Copy the input into the data buffer. | 
|  | n, _ := e.dataBuffer.Write(data) | 
|  | // Process only enough data to form a complete hash block. This may | 
|  | // leave excess data in e.dbuf to be processed later on. | 
|  | dcnt, err := e.processDataBuffer(false) | 
|  | if err != nil { | 
|  | return n, fmt.Errorf("while processing the data buffer: %w", err) | 
|  | } | 
|  | // Update the superblock with the count of data blocks written. | 
|  | e.sb.dataBlocks += dcnt | 
|  | return n, nil | 
|  | } | 
|  |  | 
|  | // Close builds a complete hash tree based on cached bottom level blocks, | 
|  | // then writes it to a preconfigured io.Writer object. A Verity superblock | 
|  | // is written, if e.writeSb is true. No data, nor the superblock is written | 
|  | // if the encoder is empty. | 
|  | // It returns an error, if one was encountered. | 
|  | func (e *encoder) Close() error { | 
|  | // Process all buffered data, including data blocks that may not form | 
|  | // a complete hash block. | 
|  | dcnt, err := e.processDataBuffer(true) | 
|  | if err != nil { | 
|  | return fmt.Errorf("while processing the data buffer: %w", err) | 
|  | } | 
|  | // Update the superblock with the count of data blocks written. | 
|  | e.sb.dataBlocks += dcnt | 
|  |  | 
|  | // Don't write anything if nothing was written to the encoder. | 
|  | if e.bottom.Len() == 0 { | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // Compute remaining hash tree levels based on the bottom level: e.bottom. | 
|  | ht, err := e.computeHashTree() | 
|  | if err != nil { | 
|  | return fmt.Errorf("while encoding a hash tree: %w", err) | 
|  | } | 
|  |  | 
|  | // Write the Verity superblock if the encoder was configured to do so. | 
|  | if e.writeSb { | 
|  | if _, err = e.sb.WriteTo(e.out); err != nil { | 
|  | return fmt.Errorf("while writing a superblock: %w", err) | 
|  | } | 
|  | } | 
|  | // Write the hash tree. | 
|  | _, err = ht.WriteTo(e.out) | 
|  | if err != nil { | 
|  | return fmt.Errorf("while writing a hash tree: %w", err) | 
|  | } | 
|  |  | 
|  | // Reset the encoder. | 
|  | e, err = NewEncoder(e.out, e.sb.dataBlockSize, e.sb.hashBlockSize, e.writeSb) | 
|  | if err != nil { | 
|  | return fmt.Errorf("while resetting an encoder: %w", err) | 
|  | } | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // MappingTable returns a complete, string-convertible Verity target mapping | 
|  | // table for use with Device Mapper, or an error. Close must be called on the | 
|  | // encoder before calling this function. dataDevicePath, hashDevicePath, and | 
|  | // hashStart parameters are parts of the mapping table. See: | 
|  | // https://www.kernel.org/doc/html/latest/admin-guide/device-mapper/verity.html | 
|  | func (e *encoder) MappingTable(dataDevicePath, hashDevicePath string, hashStart int64) (*MappingTable, error) { | 
|  | if e.rootHash == nil { | 
|  | if e.bottom.Len() != 0 { | 
|  | return nil, fmt.Errorf("encoder wasn't closed.") | 
|  | } | 
|  | return nil, fmt.Errorf("encoder is empty.") | 
|  | } | 
|  |  | 
|  | if e.writeSb { | 
|  | // Account for the superblock. | 
|  | hashStart += 1 | 
|  | } | 
|  | return &MappingTable{ | 
|  | superblock:     e.sb, | 
|  | DataDevicePath: dataDevicePath, | 
|  | HashDevicePath: hashDevicePath, | 
|  | HashStart:      hashStart, | 
|  | rootHash:       e.rootHash, | 
|  | }, nil | 
|  | } |