From 902babdf22199d73171716e643f1ffbb65e6fb48 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sun, 1 Nov 2015 12:11:36 +0100 Subject: [PATCH] Refactor ciphertext <-> plaintext offset translation functions Move all the intelligence into the new file address_translation.go. That the calculations were spread out too much became apparent when adding the file header. This should make the code much easier to modify in the future. --- cryptfs/address_translation.go | 79 ++++++++++++++++++++++ cryptfs/config_file.go | 2 +- cryptfs/content_test.go | 22 ++++--- cryptfs/cryptfs.go | 7 +- cryptfs/cryptfs_content.go | 115 --------------------------------- cryptfs/file_header.go | 10 +-- cryptfs/intrablock.go | 16 ++++- main_test.go | 26 ++++---- pathfs_frontend/file.go | 50 +++++++------- pathfs_frontend/file_holes.go | 4 +- pathfs_frontend/fs.go | 2 +- test.bash | 11 ++-- 12 files changed, 158 insertions(+), 186 deletions(-) create mode 100644 cryptfs/address_translation.go diff --git a/cryptfs/address_translation.go b/cryptfs/address_translation.go new file mode 100644 index 0000000..dfc6ef9 --- /dev/null +++ b/cryptfs/address_translation.go @@ -0,0 +1,79 @@ +package cryptfs + +// CryptFS methods that translate offsets between ciphertext and plaintext + +// get the block number at plain-text offset +func (be *CryptFS) PlainOffToBlockNo(plainOffset uint64) uint64 { + return plainOffset / be.plainBS +} + +// get the block number at ciphter-text offset +func (be *CryptFS) CipherOffToBlockNo(cipherOffset uint64) uint64 { + return (cipherOffset - HEADER_LEN) / be.cipherBS +} + +// get ciphertext offset of block "blockNo" +func (be *CryptFS) BlockNoToCipherOff(blockNo uint64) uint64 { + return HEADER_LEN + blockNo*be.cipherBS +} + +// get plaintext offset of block "blockNo" +func (be *CryptFS) BlockNoToPlainOff(blockNo uint64) uint64 { + return blockNo * be.plainBS +} + +// PlainSize - calculate plaintext size from ciphertext size +func (be *CryptFS) CipherSizeToPlainSize(cipherSize uint64) uint64 { + + // Zero sized files stay zero-sized + if cipherSize == 0 { + return 0 + } + + // Block number at last byte + blockNo := be.CipherOffToBlockNo(cipherSize - 1) + blockCount := blockNo + 1 + + overhead := BLOCK_OVERHEAD*blockCount + HEADER_LEN + + return cipherSize - overhead +} + +// CipherSize - calculate ciphertext size from plaintext size +func (be *CryptFS) PlainSizeToCipherSize(plainSize uint64) uint64 { + + // Block number at last byte + blockNo := be.PlainOffToBlockNo(plainSize - 1) + blockCount := blockNo + 1 + + overhead := BLOCK_OVERHEAD*blockCount + HEADER_LEN + + return plainSize + overhead +} + +// Split a plaintext byte range into (possibly partial) blocks +func (be *CryptFS) ExplodePlainRange(offset uint64, length uint64) []intraBlock { + var blocks []intraBlock + var nextBlock intraBlock + nextBlock.fs = be + + for length > 0 { + nextBlock.BlockNo = be.PlainOffToBlockNo(offset) + nextBlock.Skip = offset - be.BlockNoToPlainOff(nextBlock.BlockNo) + + // Minimum of remaining data and remaining space in the block + nextBlock.Length = MinUint64(length, be.plainBS-nextBlock.Skip) + + blocks = append(blocks, nextBlock) + offset += nextBlock.Length + length -= nextBlock.Length + } + return blocks +} + +func MinUint64(x uint64, y uint64) uint64 { + if x < y { + return x + } + return y +} diff --git a/cryptfs/config_file.go b/cryptfs/config_file.go index 7e762ad..16a3eec 100644 --- a/cryptfs/config_file.go +++ b/cryptfs/config_file.go @@ -1,8 +1,8 @@ package cryptfs import ( - "fmt" "encoding/json" + "fmt" "io/ioutil" ) import "os" diff --git a/cryptfs/content_test.go b/cryptfs/content_test.go index 4e1b447..37635f0 100644 --- a/cryptfs/content_test.go +++ b/cryptfs/content_test.go @@ -16,7 +16,7 @@ func TestSplitRange(t *testing.T) { testRange{0, 10}, testRange{234, 6511}, testRange{65444, 54}, - testRange{0, 1024*1024}, + testRange{0, 1024 * 1024}, testRange{0, 65536}, testRange{6654, 8945}) @@ -24,8 +24,8 @@ func TestSplitRange(t *testing.T) { f := NewCryptFS(key, true) for _, r := range ranges { - parts := f.SplitRange(r.offset, r.length) - var lastBlockNo uint64 = 1<<63 + parts := f.ExplodePlainRange(r.offset, r.length) + var lastBlockNo uint64 = 1 << 63 for _, p := range parts { if p.BlockNo == lastBlockNo { t.Errorf("Duplicate block number %d", p.BlockNo) @@ -51,11 +51,15 @@ func TestCiphertextRange(t *testing.T) { f := NewCryptFS(key, true) for _, r := range ranges { - alignedOffset, alignedLength, skipBytes := f.CiphertextRange(r.offset, r.length) + + blocks := f.ExplodePlainRange(r.offset, r.length) + alignedOffset, alignedLength := blocks[0].JointCiphertextRange(blocks) + skipBytes := blocks[0].Skip + if alignedLength < r.length { t.Errorf("alignedLength=%s is smaller than length=%d", alignedLength, r.length) } - if (alignedOffset - HEADER_LEN)%f.cipherBS != 0 { + if (alignedOffset-HEADER_LEN)%f.cipherBS != 0 { t.Errorf("alignedOffset=%d is not aligned", alignedOffset) } if r.offset%f.plainBS != 0 && skipBytes == 0 { @@ -68,19 +72,19 @@ func TestBlockNo(t *testing.T) { key := make([]byte, KEY_LEN) f := NewCryptFS(key, true) - b := f.BlockNoCipherOff(788) + b := f.CipherOffToBlockNo(788) if b != 0 { t.Errorf("actual: %d", b) } - b = f.BlockNoCipherOff(HEADER_LEN + f.CipherBS()) + b = f.CipherOffToBlockNo(HEADER_LEN + f.cipherBS) if b != 1 { t.Errorf("actual: %d", b) } - b = f.BlockNoPlainOff(788) + b = f.PlainOffToBlockNo(788) if b != 0 { t.Errorf("actual: %d", b) } - b = f.BlockNoPlainOff(f.PlainBS()) + b = f.PlainOffToBlockNo(f.plainBS) if b != 1 { t.Errorf("actual: %d", b) } diff --git a/cryptfs/cryptfs.go b/cryptfs/cryptfs.go index 0593214..9fe492d 100644 --- a/cryptfs/cryptfs.go +++ b/cryptfs/cryptfs.go @@ -13,7 +13,7 @@ const ( KEY_LEN = 32 // AES-256 NONCE_LEN = 12 AUTH_TAG_LEN = 16 - BLOCK_OVERHEAD = NONCE_LEN + AUTH_TAG_LEN + BLOCK_OVERHEAD = NONCE_LEN + AUTH_TAG_LEN ) type CryptFS struct { @@ -61,8 +61,3 @@ func NewCryptFS(key []byte, useOpenssl bool) *CryptFS { func (be *CryptFS) PlainBS() uint64 { return be.plainBS } - -// Get ciphertext block size -func (be *CryptFS) CipherBS() uint64 { - return be.cipherBS -} diff --git a/cryptfs/cryptfs_content.go b/cryptfs/cryptfs_content.go index 03253d3..d74570f 100644 --- a/cryptfs/cryptfs_content.go +++ b/cryptfs/cryptfs_content.go @@ -12,11 +12,6 @@ import ( "os" ) -const ( - // A block of 4124 zero bytes has this md5 - ZeroBlockMd5 = "64331af89bd15a987b39855338336237" -) - // md5sum - debug helper, return md5 hex string func md5sum(buf []byte) string { rawHash := md5.Sum(buf) @@ -110,106 +105,6 @@ func (be *CryptFS) EncryptBlock(plaintext []byte, blockNo uint64, fileId []byte) return ciphertext } -// Split a plaintext byte range into (possibly partial) blocks -func (be *CryptFS) SplitRange(offset uint64, length uint64) []intraBlock { - var b intraBlock - var parts []intraBlock - - b.fs = be - - for length > 0 { - b.BlockNo = offset / be.plainBS - b.Skip = offset % be.plainBS - // Minimum of remaining data and remaining space in the block - b.Length = be.minu64(length, be.plainBS-b.Skip) - parts = append(parts, b) - offset += b.Length - length -= b.Length - } - return parts -} - -// PlainSize - calculate plaintext size from ciphertext size -func (be *CryptFS) PlainSize(size uint64) uint64 { - - // Zero sized files stay zero-sized - if size == 0 { - return 0 - } - - // Account for header - size -= HEADER_LEN - - overhead := be.cipherBS - be.plainBS - nBlocks := (size + be.cipherBS - 1) / be.cipherBS - if nBlocks*overhead > size { - Warn.Printf("PlainSize: Negative size, returning 0 instead\n") - return 0 - } - size -= nBlocks * overhead - - return size -} - -// CipherSize - calculate ciphertext size from plaintext size -func (be *CryptFS) CipherSize(size uint64) uint64 { - overhead := be.cipherBS - be.plainBS - nBlocks := (size + be.plainBS - 1) / be.plainBS - size += nBlocks * overhead - - return size -} - -func (be *CryptFS) minu64(x uint64, y uint64) uint64 { - if x < y { - return x - } - return y -} - -// CiphertextRange - Get byte range in backing ciphertext corresponding -// to plaintext range. Returns a range aligned to ciphertext blocks. -func (be *CryptFS) CiphertextRange(offset uint64, length uint64) (alignedOffset uint64, alignedLength uint64, skipBytes int) { - // Decrypting the ciphertext will yield too many plaintext bytes. Skip this number - // of bytes from the front. - skip := offset % be.plainBS - - firstBlockNo := offset / be.plainBS - lastBlockNo := (offset + length - 1) / be.plainBS - - alignedOffset = HEADER_LEN + firstBlockNo * be.cipherBS - alignedLength = (lastBlockNo - firstBlockNo + 1) * be.cipherBS - - skipBytes = int(skip) - return alignedOffset, alignedLength, skipBytes -} - -// Get the byte range in the ciphertext corresponding to blocks -// (full blocks!) -func (be *CryptFS) JoinCiphertextRange(blocks []intraBlock) (uint64, uint64) { - - offset, _ := blocks[0].CiphertextRange() - last := blocks[len(blocks)-1] - length := (last.BlockNo - blocks[0].BlockNo + 1) * be.cipherBS - - return offset, length -} - -// Crop plaintext that correspons to complete cipher blocks down to what is -// requested according to "iblocks" -func (be *CryptFS) CropPlaintext(plaintext []byte, blocks []intraBlock) []byte { - offset := blocks[0].Skip - last := blocks[len(blocks)-1] - length := (last.BlockNo - blocks[0].BlockNo + 1) * be.plainBS - var cropped []byte - if offset+length > uint64(len(plaintext)) { - cropped = plaintext[offset:] - } else { - cropped = plaintext[offset : offset+length] - } - return cropped -} - // MergeBlocks - Merge newData into oldData at offset // New block may be bigger than both newData and oldData func (be *CryptFS) MergeBlocks(oldData []byte, newData []byte, offset int) []byte { @@ -230,13 +125,3 @@ func (be *CryptFS) MergeBlocks(oldData []byte, newData []byte, offset int) []byt } return out[0:outLen] } - -// Get the block number at plain-text offset -func (be *CryptFS) BlockNoPlainOff(plainOffset uint64) uint64 { - return plainOffset / be.plainBS -} - -// Get the block number at ciphter-text offset -func (be *CryptFS) BlockNoCipherOff(cipherOffset uint64) uint64 { - return (cipherOffset - HEADER_LEN) / be.cipherBS -} diff --git a/cryptfs/file_header.go b/cryptfs/file_header.go index 3fd7266..e16cbab 100644 --- a/cryptfs/file_header.go +++ b/cryptfs/file_header.go @@ -10,15 +10,15 @@ import ( ) const ( - HEADER_CURRENT_VERSION = 1 // Current on-disk-format version - HEADER_VERSION_LEN = 2 // uint16 - HEADER_ID_LEN = 16 // 128 bit random file id - HEADER_LEN = HEADER_VERSION_LEN + HEADER_ID_LEN // Total header length + HEADER_CURRENT_VERSION = 1 // Current on-disk-format version + HEADER_VERSION_LEN = 2 // uint16 + HEADER_ID_LEN = 16 // 128 bit random file id + HEADER_LEN = HEADER_VERSION_LEN + HEADER_ID_LEN // Total header length ) type FileHeader struct { Version uint16 - Id []byte + Id []byte } // Pack - serialize fileHeader object diff --git a/cryptfs/intrablock.go b/cryptfs/intrablock.go index c83976c..faff471 100644 --- a/cryptfs/intrablock.go +++ b/cryptfs/intrablock.go @@ -19,13 +19,13 @@ func (ib *intraBlock) IsPartial() bool { // CiphertextRange - get byte range in ciphertext file corresponding to BlockNo // (complete block) func (ib *intraBlock) CiphertextRange() (offset uint64, length uint64) { - return HEADER_LEN + ib.BlockNo * ib.fs.cipherBS, ib.fs.cipherBS + return ib.fs.BlockNoToCipherOff(ib.BlockNo), ib.fs.cipherBS } // PlaintextRange - get byte range in plaintext corresponding to BlockNo // (complete block) func (ib *intraBlock) PlaintextRange() (offset uint64, length uint64) { - return ib.BlockNo * ib.fs.plainBS, ib.fs.plainBS + return ib.fs.BlockNoToPlainOff(ib.BlockNo), ib.fs.plainBS } // CropBlock - crop a potentially larger plaintext block down to the relevant part @@ -37,3 +37,15 @@ func (ib *intraBlock) CropBlock(d []byte) []byte { } return d[ib.Skip:lenWant] } + +// Ciphertext range corresponding to the sum of all "blocks" (complete blocks) +func (ib *intraBlock) JointCiphertextRange(blocks []intraBlock) (offset uint64, length uint64) { + firstBlock := blocks[0] + lastBlock := blocks[len(blocks)-1] + + offset = ib.fs.BlockNoToCipherOff(firstBlock.BlockNo) + offsetLast := ib.fs.BlockNoToCipherOff(lastBlock.BlockNo) + length = offsetLast + ib.fs.cipherBS - offset + + return offset, length +} diff --git a/main_test.go b/main_test.go index 287c792..9262e6f 100644 --- a/main_test.go +++ b/main_test.go @@ -1,8 +1,6 @@ package main import ( - "runtime" - "sync" "bytes" "crypto/md5" "encoding/hex" @@ -11,6 +9,8 @@ import ( "io/ioutil" "os" "os/exec" + "runtime" + "sync" "testing" ) @@ -121,7 +121,7 @@ func testWriteN(t *testing.T, fn string, n int) string { } file.Close() - verifySize(t, plainDir + fn, n) + verifySize(t, plainDir+fn, n) bin := md5.Sum(d) hashWant := hex.EncodeToString(bin[:]) @@ -244,12 +244,12 @@ func TestFileHoles(t *testing.T) { } func sContains(haystack []string, needle string) bool { - for _, element := range haystack { - if element == needle { - return true - } - } - return false + for _, element := range haystack { + if element == needle { + return true + } + } + return false } func TestRmwRace(t *testing.T) { @@ -313,10 +313,10 @@ func TestRmwRace(t *testing.T) { goodMd5[m] = goodMd5[m] + 1 /* - if m == "6c1660fdabccd448d1359f27b3db3c99" { - fmt.Println(hex.Dump(buf)) - t.FailNow() - } + if m == "6c1660fdabccd448d1359f27b3db3c99" { + fmt.Println(hex.Dump(buf)) + t.FailNow() + } */ } fmt.Println(goodMd5) diff --git a/pathfs_frontend/file.go b/pathfs_frontend/file.go index 438fe77..e8d7003 100644 --- a/pathfs_frontend/file.go +++ b/pathfs_frontend/file.go @@ -128,8 +128,10 @@ func (f *file) doRead(off uint64, length uint64) ([]byte, fuse.Status) { } // Read the backing ciphertext in one go - alignedOffset, alignedLength, skip := f.cfs.CiphertextRange(off, length) - cryptfs.Debug.Printf("CiphertextRange(%d, %d) -> %d, %d, %d\n", off, length, alignedOffset, alignedLength, skip) + blocks := f.cfs.ExplodePlainRange(off, length) + alignedOffset, alignedLength := blocks[0].JointCiphertextRange(blocks) + skip := blocks[0].Skip + cryptfs.Debug.Printf("JointCiphertextRange(%d, %d) -> %d, %d, %d\n", off, length, alignedOffset, alignedLength, skip) ciphertext := make([]byte, int(alignedLength)) f.fdLock.Lock() n, err := f.fd.ReadAt(ciphertext, int64(alignedOffset)) @@ -141,27 +143,27 @@ func (f *file) doRead(off uint64, length uint64) ([]byte, fuse.Status) { // Truncate ciphertext buffer down to actually read bytes ciphertext = ciphertext[0:n] - blockNo := (alignedOffset - cryptfs.HEADER_LEN) / f.cfs.CipherBS() - cryptfs.Debug.Printf("ReadAt offset=%d bytes (%d blocks), want=%d, got=%d\n", alignedOffset, blockNo, alignedLength, n) + firstBlockNo := blocks[0].BlockNo + cryptfs.Debug.Printf("ReadAt offset=%d bytes (%d blocks), want=%d, got=%d\n", alignedOffset, firstBlockNo, alignedLength, n) // Decrypt it - plaintext, err := f.cfs.DecryptBlocks(ciphertext, blockNo, f.header.Id) + plaintext, err := f.cfs.DecryptBlocks(ciphertext, firstBlockNo, f.header.Id) if err != nil { - blockNo := (alignedOffset + uint64(len(plaintext))) / f.cfs.PlainBS() - cipherOff := cryptfs.HEADER_LEN + blockNo * f.cfs.CipherBS() - plainOff := blockNo * f.cfs.PlainBS() - cryptfs.Warn.Printf("ino%d: doRead: corrupt block #%d (plainOff=%d/%d, cipherOff=%d/%d)\n", - f.ino, blockNo, plainOff, f.cfs.PlainBS(), cipherOff, f.cfs.CipherBS()) + curruptBlockNo := firstBlockNo + f.cfs.PlainOffToBlockNo(uint64(len(plaintext))) + cipherOff := f.cfs.BlockNoToCipherOff(curruptBlockNo) + plainOff := f.cfs.BlockNoToPlainOff(curruptBlockNo) + cryptfs.Warn.Printf("ino%d: doRead: corrupt block #%d (plainOff=%d, cipherOff=%d)\n", + f.ino, curruptBlockNo, plainOff, cipherOff) return nil, fuse.EIO } // Crop down to the relevant part var out []byte lenHave := len(plaintext) - lenWant := skip + int(length) + lenWant := int(skip + length) if lenHave > lenWant { - out = plaintext[skip : skip+int(length)] - } else if lenHave > skip { + out = plaintext[skip:lenWant] + } else if lenHave > int(skip) { out = plaintext[skip:lenHave] } // else: out stays empty, file was smaller than the requested offset @@ -216,7 +218,7 @@ func (f *file) doWrite(data []byte, off int64) (uint32, fuse.Status) { var written uint32 status := fuse.OK dataBuf := bytes.NewBuffer(data) - blocks := f.cfs.SplitRange(uint64(off), uint64(len(data))) + blocks := f.cfs.ExplodePlainRange(uint64(off), uint64(len(data))) for _, b := range blocks { blockData := dataBuf.Next(int(b.Length)) @@ -239,11 +241,7 @@ func (f *file) doWrite(data []byte, off int64) (uint32, fuse.Status) { blockOffset, _ := b.CiphertextRange() blockData = f.cfs.EncryptBlock(blockData, b.BlockNo, f.header.Id) cryptfs.Debug.Printf("ino%d: Writing %d bytes to block #%d, md5=%s\n", - f.ino, len(blockData) - cryptfs.BLOCK_OVERHEAD, b.BlockNo, cryptfs.Debug.Md5sum(blockData)) - if len(blockData) != int(f.cfs.CipherBS()) { - cryptfs.Debug.Printf("ino%d: Writing partial block #%d (%d bytes)\n", - f.ino, b.BlockNo, len(blockData) - cryptfs.BLOCK_OVERHEAD) - } + f.ino, len(blockData)-cryptfs.BLOCK_OVERHEAD, b.BlockNo, cryptfs.Debug.Md5sum(blockData)) f.fdLock.Lock() _, err := f.fd.WriteAt(blockData, int64(blockOffset)) f.fdLock.Unlock() @@ -267,7 +265,7 @@ func (f *file) Write(data []byte, off int64) (uint32, fuse.Status) { cryptfs.Warn.Printf("Write: Fstat failed: %v\n", err) return 0, fuse.ToStatus(err) } - plainSize := f.cfs.PlainSize(uint64(fi.Size())) + plainSize := f.cfs.CipherSizeToPlainSize(uint64(fi.Size())) if f.createsHole(plainSize, off) { status := f.zeroPad(plainSize) if status != fuse.OK { @@ -332,7 +330,7 @@ func (f *file) Truncate(newSize uint64) fuse.Status { cryptfs.Warn.Printf("Truncate: Fstat failed: %v\n", err) return fuse.ToStatus(err) } - oldSize := f.cfs.PlainSize(uint64(fi.Size())) + oldSize := f.cfs.CipherSizeToPlainSize(uint64(fi.Size())) { oldB := float32(oldSize) / float32(f.cfs.PlainBS()) newB := float32(newSize) / float32(f.cfs.PlainBS()) @@ -350,7 +348,7 @@ func (f *file) Truncate(newSize uint64) fuse.Status { } } - blocks := f.cfs.SplitRange(oldSize, newSize-oldSize) + blocks := f.cfs.ExplodePlainRange(oldSize, newSize-oldSize) for _, b := range blocks { // First and last block may be partial if b.IsPartial() { @@ -374,9 +372,9 @@ func (f *file) Truncate(newSize uint64) fuse.Status { return fuse.OK } else { // File shrinks - blockNo := f.cfs.BlockNoPlainOff(newSize) - cipherOff := cryptfs.HEADER_LEN + blockNo * f.cfs.CipherBS() - plainOff := blockNo * f.cfs.PlainBS() + blockNo := f.cfs.PlainOffToBlockNo(newSize) + cipherOff := f.cfs.BlockNoToCipherOff(blockNo) + plainOff := f.cfs.BlockNoToPlainOff(blockNo) lastBlockLen := newSize - plainOff var data []byte if lastBlockLen > 0 { @@ -430,7 +428,7 @@ func (f *file) GetAttr(a *fuse.Attr) fuse.Status { return fuse.ToStatus(err) } a.FromStat(&st) - a.Size = f.cfs.PlainSize(a.Size) + a.Size = f.cfs.CipherSizeToPlainSize(a.Size) return fuse.OK } diff --git a/pathfs_frontend/file_holes.go b/pathfs_frontend/file_holes.go index f906aa6..3db4828 100644 --- a/pathfs_frontend/file_holes.go +++ b/pathfs_frontend/file_holes.go @@ -7,8 +7,8 @@ import ( // Will a write to offset "off" create a file hole? func (f *file) createsHole(plainSize uint64, off int64) bool { - nextBlock := f.cfs.BlockNoPlainOff(plainSize) - targetBlock := f.cfs.BlockNoPlainOff(uint64(off)) + nextBlock := f.cfs.PlainOffToBlockNo(plainSize) + targetBlock := f.cfs.PlainOffToBlockNo(uint64(off)) if targetBlock > nextBlock { return true } diff --git a/pathfs_frontend/fs.go b/pathfs_frontend/fs.go index eebc87b..3ec503f 100644 --- a/pathfs_frontend/fs.go +++ b/pathfs_frontend/fs.go @@ -41,7 +41,7 @@ func (fs *FS) GetAttr(name string, context *fuse.Context) (*fuse.Attr, fuse.Stat return a, status } if a.IsRegular() { - a.Size = fs.PlainSize(a.Size) + a.Size = fs.CipherSizeToPlainSize(a.Size) } else if a.IsSymlink() { target, _ := fs.Readlink(name, context) a.Size = uint64(len(target)) diff --git a/test.bash b/test.bash index fb849c4..976bb02 100755 --- a/test.bash +++ b/test.bash @@ -2,11 +2,10 @@ set -eux -cd cryptfs -go build -go test -cd .. +for i in ./cryptfs . +do -go build -go test + go build $i + go test $i +done