diff options
| -rw-r--r-- | README.md | 16 | ||||
| -rw-r--r-- | SECURITY.md | 9 | ||||
| -rw-r--r-- | cryptfs/config_file.go | 14 | ||||
| -rw-r--r-- | cryptfs/content_test.go | 21 | ||||
| -rw-r--r-- | cryptfs/cryptfs.go | 1 | ||||
| -rw-r--r-- | cryptfs/cryptfs_content.go | 20 | ||||
| -rw-r--r-- | cryptfs/file_header.go | 56 | ||||
| -rw-r--r-- | cryptfs/intrablock.go | 2 | ||||
| -rw-r--r-- | pathfs_frontend/file.go | 179 | ||||
| -rw-r--r-- | pathfs_frontend/fs.go | 3 | 
10 files changed, 260 insertions, 61 deletions
| @@ -62,3 +62,19 @@ The output should look like this:  	BenchmarkStreamRead 	     200	   7848155 ns/op	 133.61 MB/s  	ok  	github.com/rfjakob/gocryptfs	9.407s +Changelog +--------- + +v0.3 (in progress) +* Add file header that contains a random id to authenticate blocks + * This is an on-disk-format change + +v0.2 +* Replace bash daemonization wrapper with native Go implementation +* Better user feedback on mount failures + +v0.1 +* First release + +See https://github.com/rfjakob/gocryptfs/releases for the release dates +and associated tags. diff --git a/SECURITY.md b/SECURITY.md index dcd16c5..2e6f3f4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -52,12 +52,11 @@ unless you have the key. The opposite of integrity is *malleability*.   * This means that any modification inside a block will be detected when reading     the block and decryption will be aborted. The failure is logged and an     I/O error is returned to the user. -* Each block uses its block number as GCM *authentication data* +* Every file has a header that contains a 16-byte random *file id* +* Each block uses the file id and its block number as GCM *authentication data*   * This means the position of the blocks is protected as well. The blocks -   can not be reordered without causing an decryption error. -* However, proper affiliation of a block to the file is can not be verified. - * This means that blocks can be copied between different files provided -   that they stay at the same position.  +   can not be reordered or copied between different files without +   causing an decryption error.  * For technical reasons (sparse files), the special "all-zero" block is    always seen as a valid block that decrypts to all-zero plaintext.   * This means that whole blocks can be zeroed out diff --git a/cryptfs/config_file.go b/cryptfs/config_file.go index ef426ff..7e762ad 100644 --- a/cryptfs/config_file.go +++ b/cryptfs/config_file.go @@ -1,6 +1,7 @@  package cryptfs  import ( +	"fmt"  	"encoding/json"  	"io/ioutil"  ) @@ -19,6 +20,8 @@ type ConfFile struct {  	EncryptedKey []byte  	// Stores parameters for scrypt hashing (key derivation)  	ScryptObject scryptKdf +	// The On-Disk-Format version this filesystem uses +	Version uint16  }  // CreateConfFile - create a new config with a random key encrypted with @@ -31,8 +34,11 @@ func CreateConfFile(filename string, password string) error {  	key := RandBytes(KEY_LEN)  	// Encrypt it using the password +	// This sets ScryptObject and EncryptedKey  	cf.EncryptKey(key, password) +	cf.Version = HEADER_CURRENT_VERSION +  	// Write file to disk  	err := cf.WriteFile() @@ -58,6 +64,10 @@ func LoadConfFile(filename string, password string) ([]byte, *ConfFile, error) {  		return nil, nil, err  	} +	if cf.Version != HEADER_CURRENT_VERSION { +		return nil, nil, fmt.Errorf("Unsupported version %d", cf.Version) +	} +  	// Generate derived key from password  	scryptHash := cf.ScryptObject.DeriveKey(password) @@ -65,7 +75,7 @@ func LoadConfFile(filename string, password string) ([]byte, *ConfFile, error) {  	// We use stock go GCM instead of OpenSSL here as speed is not important  	// and we get better error messages  	cfs := NewCryptFS(scryptHash, false) -	key, err := cfs.DecryptBlock(cf.EncryptedKey, 0) +	key, err := cfs.DecryptBlock(cf.EncryptedKey, 0, nil)  	if err != nil {  		Warn.Printf("failed to unlock master key: %s\n", err.Error())  		Warn.Printf("Password incorrect.\n") @@ -84,7 +94,7 @@ func (cf *ConfFile) EncryptKey(key []byte, password string) {  	// Lock master key using password-based key  	cfs := NewCryptFS(scryptHash, false) -	cf.EncryptedKey = cfs.EncryptBlock(key, 0) +	cf.EncryptedKey = cfs.EncryptBlock(key, 0, nil)  }  // WriteFile - write out config in JSON format to file "filename.tmp" diff --git a/cryptfs/content_test.go b/cryptfs/content_test.go index aa6c9f3..4e1b447 100644 --- a/cryptfs/content_test.go +++ b/cryptfs/content_test.go @@ -1,7 +1,6 @@  package cryptfs  import ( -	"fmt"  	"testing"  ) @@ -17,6 +16,8 @@ func TestSplitRange(t *testing.T) {  		testRange{0, 10},  		testRange{234, 6511},  		testRange{65444, 54}, +		testRange{0, 1024*1024}, +		testRange{0, 65536},  		testRange{6654, 8945})  	key := make([]byte, KEY_LEN) @@ -24,10 +25,14 @@ func TestSplitRange(t *testing.T) {  	for _, r := range ranges {  		parts := f.SplitRange(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) +			} +			lastBlockNo = p.BlockNo  			if p.Length > DEFAULT_PLAINBS || p.Skip >= DEFAULT_PLAINBS { -				fmt.Printf("Test fail: n=%d, length=%d, offset=%d\n", p.BlockNo, p.Length, p.Skip) -				t.Fail() +				t.Errorf("Test fail: n=%d, length=%d, offset=%d\n", p.BlockNo, p.Length, p.Skip)  			}  		}  	} @@ -48,13 +53,13 @@ func TestCiphertextRange(t *testing.T) {  	for _, r := range ranges {  		alignedOffset, alignedLength, skipBytes := f.CiphertextRange(r.offset, r.length)  		if alignedLength < r.length { -			t.Fail() +			t.Errorf("alignedLength=%s is smaller than length=%d", alignedLength, r.length)  		} -		if alignedOffset%f.cipherBS != 0 { -			t.Fail() +		if (alignedOffset - HEADER_LEN)%f.cipherBS != 0 { +			t.Errorf("alignedOffset=%d is not aligned", alignedOffset)  		}  		if r.offset%f.plainBS != 0 && skipBytes == 0 { -			t.Fail() +			t.Errorf("skipBytes=0")  		}  	}  } @@ -67,7 +72,7 @@ func TestBlockNo(t *testing.T) {  	if b != 0 {  		t.Errorf("actual: %d", b)  	} -	b = f.BlockNoCipherOff(f.CipherBS()) +	b = f.BlockNoCipherOff(HEADER_LEN + f.CipherBS())  	if b != 1 {  		t.Errorf("actual: %d", b)  	} diff --git a/cryptfs/cryptfs.go b/cryptfs/cryptfs.go index 1556757..0593214 100644 --- a/cryptfs/cryptfs.go +++ b/cryptfs/cryptfs.go @@ -13,6 +13,7 @@ const (  	KEY_LEN         = 32 // AES-256  	NONCE_LEN       = 12  	AUTH_TAG_LEN    = 16 +	BLOCK_OVERHEAD = NONCE_LEN + AUTH_TAG_LEN  )  type CryptFS struct { diff --git a/cryptfs/cryptfs_content.go b/cryptfs/cryptfs_content.go index 7a1859a..03253d3 100644 --- a/cryptfs/cryptfs_content.go +++ b/cryptfs/cryptfs_content.go @@ -30,14 +30,14 @@ type CryptFile struct {  }  // DecryptBlocks - Decrypt a number of blocks -func (be *CryptFS) DecryptBlocks(ciphertext []byte, firstBlockNo uint64) ([]byte, error) { +func (be *CryptFS) DecryptBlocks(ciphertext []byte, firstBlockNo uint64, fileId []byte) ([]byte, error) {  	cBuf := bytes.NewBuffer(ciphertext)  	var err error  	var pBuf bytes.Buffer  	for cBuf.Len() > 0 {  		cBlock := cBuf.Next(int(be.cipherBS))  		var pBlock []byte -		pBlock, err = be.DecryptBlock(cBlock, firstBlockNo) +		pBlock, err = be.DecryptBlock(cBlock, firstBlockNo, fileId)  		if err != nil {  			break  		} @@ -51,7 +51,7 @@ func (be *CryptFS) DecryptBlocks(ciphertext []byte, firstBlockNo uint64) ([]byte  //  // Corner case: A full-sized block of all-zero ciphertext bytes is translated  // to an all-zero plaintext block, i.e. file hole passtrough. -func (be *CryptFS) DecryptBlock(ciphertext []byte, blockNo uint64) ([]byte, error) { +func (be *CryptFS) DecryptBlock(ciphertext []byte, blockNo uint64, fileId []byte) ([]byte, error) {  	// Empty block?  	if len(ciphertext) == 0 { @@ -77,6 +77,7 @@ func (be *CryptFS) DecryptBlock(ciphertext []byte, blockNo uint64) ([]byte, erro  	// Decrypt  	var plaintext []byte  	aData := make([]byte, 8) +	aData = append(aData, fileId...)  	binary.BigEndian.PutUint64(aData, blockNo)  	plaintext, err := be.gcm.Open(plaintext, nonce, ciphertext, aData) @@ -89,8 +90,8 @@ func (be *CryptFS) DecryptBlock(ciphertext []byte, blockNo uint64) ([]byte, erro  	return plaintext, nil  } -// encryptBlock - Encrypt and add MAC using GCM -func (be *CryptFS) EncryptBlock(plaintext []byte, blockNo uint64) []byte { +// encryptBlock - Encrypt and add IV and MAC +func (be *CryptFS) EncryptBlock(plaintext []byte, blockNo uint64, fileId []byte) []byte {  	// Empty block?  	if len(plaintext) == 0 { @@ -103,6 +104,7 @@ func (be *CryptFS) EncryptBlock(plaintext []byte, blockNo uint64) []byte {  	// Encrypt plaintext and append to nonce  	aData := make([]byte, 8)  	binary.BigEndian.PutUint64(aData, blockNo) +	aData = append(aData, fileId...)  	ciphertext := be.gcm.Seal(nonce, nonce, plaintext, aData)  	return ciphertext @@ -118,6 +120,7 @@ func (be *CryptFS) SplitRange(offset uint64, length uint64) []intraBlock {  	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 @@ -134,6 +137,9 @@ func (be *CryptFS) PlainSize(size uint64) uint64 {  		return 0  	} +	// Account for header +	size -= HEADER_LEN +  	overhead := be.cipherBS - be.plainBS  	nBlocks := (size + be.cipherBS - 1) / be.cipherBS  	if nBlocks*overhead > size { @@ -171,7 +177,7 @@ func (be *CryptFS) CiphertextRange(offset uint64, length uint64) (alignedOffset  	firstBlockNo := offset / be.plainBS  	lastBlockNo := (offset + length - 1) / be.plainBS -	alignedOffset = firstBlockNo * be.cipherBS +	alignedOffset = HEADER_LEN + firstBlockNo * be.cipherBS  	alignedLength = (lastBlockNo - firstBlockNo + 1) * be.cipherBS  	skipBytes = int(skip) @@ -232,5 +238,5 @@ func (be *CryptFS) BlockNoPlainOff(plainOffset uint64) uint64 {  // Get the block number at ciphter-text offset  func (be *CryptFS) BlockNoCipherOff(cipherOffset uint64) uint64 { -	return cipherOffset / be.cipherBS +	return (cipherOffset - HEADER_LEN) / be.cipherBS  } diff --git a/cryptfs/file_header.go b/cryptfs/file_header.go new file mode 100644 index 0000000..3fd7266 --- /dev/null +++ b/cryptfs/file_header.go @@ -0,0 +1,56 @@ +package cryptfs + +// Per-file header +// +// Format: [ "Version" uint16 big endian ] [ "Id" 16 random bytes ] + +import ( +	"encoding/binary" +	"fmt" +) + +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 +) + +type FileHeader struct { +	Version uint16 +	Id []byte +} + +// Pack - serialize fileHeader object +func (h *FileHeader) Pack() []byte { +	if len(h.Id) != HEADER_ID_LEN || h.Version != HEADER_CURRENT_VERSION { +		panic("FileHeader object not properly initialized") +	} +	buf := make([]byte, HEADER_LEN) +	binary.BigEndian.PutUint16(buf[0:HEADER_VERSION_LEN], h.Version) +	copy(buf[HEADER_VERSION_LEN:], h.Id) +	return buf + +} + +// ParseHeader - parse "buf" into fileHeader object +func ParseHeader(buf []byte) (*FileHeader, error) { +	if len(buf) != HEADER_LEN { +		return nil, fmt.Errorf("ParseHeader: invalid length: got %d, want %d", len(buf), HEADER_LEN) +	} +	var h FileHeader +	h.Version = binary.BigEndian.Uint16(buf[0:HEADER_VERSION_LEN]) +	if h.Version != HEADER_CURRENT_VERSION { +		return nil, fmt.Errorf("ParseHeader: invalid version: got %d, want %d", h.Version, HEADER_CURRENT_VERSION) +	} +	h.Id = buf[HEADER_VERSION_LEN:] +	return &h, nil +} + +// RandomHeader - create new fileHeader object with random Id +func RandomHeader() *FileHeader { +	var h FileHeader +	h.Version = HEADER_CURRENT_VERSION +	h.Id = RandBytes(HEADER_ID_LEN) +	return &h +} diff --git a/cryptfs/intrablock.go b/cryptfs/intrablock.go index 7f3a1eb..c83976c 100644 --- a/cryptfs/intrablock.go +++ b/cryptfs/intrablock.go @@ -19,7 +19,7 @@ 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 ib.BlockNo * ib.fs.cipherBS, ib.fs.cipherBS +	return HEADER_LEN + ib.BlockNo * ib.fs.cipherBS, ib.fs.cipherBS  }  // PlaintextRange - get byte range in plaintext corresponding to BlockNo diff --git a/pathfs_frontend/file.go b/pathfs_frontend/file.go index 3304267..438fe77 100644 --- a/pathfs_frontend/file.go +++ b/pathfs_frontend/file.go @@ -23,7 +23,7 @@ type file struct {  	// reuse the fd number after it is closed. When open races  	// with another close, they may lead to confusion as which  	// file gets written in the end. -	lock sync.Mutex +	fdLock sync.Mutex  	// Was the file opened O_WRONLY?  	writeOnly bool @@ -33,6 +33,9 @@ type file struct {  	// Inode number  	ino uint64 + +	// File header +	header *cryptfs.FileHeader  }  func NewFile(fd *os.File, writeOnly bool, cfs *cryptfs.CryptFS) nodefs.File { @@ -54,26 +57,83 @@ func (f *file) InnerFile() nodefs.File {  func (f *file) SetInode(n *nodefs.Inode) {  } +// Ensure that all modifications to the file contents are serialized and no +// reads happen concurrently. +// +// This prevents several races: +// * getFileId vs Truncate +// * zeroPad vs Read +// * RMW vs Write +func (f *file) wlock() { +} +func (f *file) rlock() { +} +func (f *file) unlock() { +} + +// readHeader - load the file header from disk +// +// Returns io.EOF if the file is empty +func (f *file) readHeader() error { +	buf := make([]byte, cryptfs.HEADER_LEN) +	_, err := f.fd.ReadAt(buf, 0) +	if err != nil { +		return err +	} +	h, err := cryptfs.ParseHeader(buf) +	if err != nil { +		return err +	} +	f.header = h + +	return nil +} + +// createHeader - create a new random header and write it to disk +func (f *file) createHeader() error { +	h := cryptfs.RandomHeader() +	buf := h.Pack() +	_, err := f.fd.WriteAt(buf, 0) +	if err != nil { +		return err +	} +	f.header = h + +	return nil +} +  func (f *file) String() string {  	return fmt.Sprintf("cryptFile(%s)", f.fd.Name())  }  // doRead - returns "length" plaintext bytes from plaintext offset "off". -// Arguments "length" and "off" do not have to be aligned. +// Arguments "length" and "off" do not have to be block-aligned.  // -// doRead reads the corresponding ciphertext blocks from disk, decryptfs them and +// doRead reads the corresponding ciphertext blocks from disk, decrypts them and  // returns the requested part of the plaintext.  // -// Called by Read() and by Write() and Truncate() for RMW +// Called by Read() for normal reading, +// by Write() and Truncate() for Read-Modify-Write  func (f *file) doRead(off uint64, length uint64) ([]byte, fuse.Status) { +	// Read file header +	if f.header == nil { +		err := f.readHeader() +		if err == io.EOF { +			return nil, fuse.OK +		} +		if err != nil { +			return nil, fuse.ToStatus(err) +		} +	} +  	// 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)  	ciphertext := make([]byte, int(alignedLength)) -	f.lock.Lock() +	f.fdLock.Lock()  	n, err := f.fd.ReadAt(ciphertext, int64(alignedOffset)) -	f.lock.Unlock() +	f.fdLock.Unlock()  	if err != nil && err != io.EOF {  		cryptfs.Warn.Printf("read: ReadAt: %s\n", err.Error())  		return nil, fuse.ToStatus(err) @@ -81,14 +141,14 @@ 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 / f.cfs.CipherBS() +	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)  	// Decrypt it -	plaintext, err := f.cfs.DecryptBlocks(ciphertext, blockNo) +	plaintext, err := f.cfs.DecryptBlocks(ciphertext, blockNo, f.header.Id)  	if err != nil {  		blockNo := (alignedOffset + uint64(len(plaintext))) / f.cfs.PlainBS() -		cipherOff := blockNo * f.cfs.CipherBS() +		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()) @@ -103,15 +163,15 @@ func (f *file) doRead(off uint64, length uint64) ([]byte, fuse.Status) {  		out = plaintext[skip : skip+int(length)]  	} else if lenHave > skip {  		out = plaintext[skip:lenHave] -	} else { -		// Out stays empty, file was smaller than the requested offset  	} +	// else: out stays empty, file was smaller than the requested offset  	return out, fuse.OK  }  // Read - FUSE call  func (f *file) Read(buf []byte, off int64) (resultData fuse.ReadResult, code fuse.Status) { +  	cryptfs.Debug.Printf("ino%d: FUSE Read: offset=%d length=%d\n", f.ino, len(buf), off)  	if f.writeOnly { @@ -132,8 +192,27 @@ func (f *file) Read(buf []byte, off int64) (resultData fuse.ReadResult, code fus  	return fuse.ReadResultData(out), status  } -// Do the actual write +// doWrite - encrypt "data" and write it to plaintext offset "off" +// +// Arguments do not have to be block-aligned, read-modify-write is +// performed internally as neccessary +// +// Called by Write() for normal writing, +// and by Truncate() to rewrite the last file block.  func (f *file) doWrite(data []byte, off int64) (uint32, fuse.Status) { + +	// Read header from disk, create a new one if the file is empty +	if f.header == nil { +		err := f.readHeader() +		if err == io.EOF { +			err = f.createHeader() + +		} +		if err != nil { +			return 0, fuse.ToStatus(err) +		} +	} +  	var written uint32  	status := fuse.OK  	dataBuf := bytes.NewBuffer(data) @@ -158,14 +237,16 @@ func (f *file) doWrite(data []byte, off int64) (uint32, fuse.Status) {  		// Write  		blockOffset, _ := b.CiphertextRange() -		blockData = f.cfs.EncryptBlock(blockData, b.BlockNo) -		cryptfs.Debug.Printf("ino%d: Writing %d bytes to block #%d, md5=%s\n", f.ino, len(blockData), b.BlockNo, cryptfs.Debug.Md5sum(blockData)) +		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.Debug.Printf("ino%d: Writing partial block #%d (%d bytes)\n", +				f.ino, b.BlockNo, len(blockData) - cryptfs.BLOCK_OVERHEAD)  		} -		f.lock.Lock() +		f.fdLock.Lock()  		_, err := f.fd.WriteAt(blockData, int64(blockOffset)) -		f.lock.Unlock() +		f.fdLock.Unlock()  		if err != nil {  			cryptfs.Warn.Printf("Write failed: %s\n", err.Error()) @@ -199,20 +280,20 @@ func (f *file) Write(data []byte, off int64) (uint32, fuse.Status) {  // Release - FUSE call, forget file  func (f *file) Release() { -	f.lock.Lock() +	f.fdLock.Lock()  	f.fd.Close() -	f.lock.Unlock() +	f.fdLock.Unlock()  }  // Flush - FUSE call  func (f *file) Flush() fuse.Status { -	f.lock.Lock() +	f.fdLock.Lock()  	// Since Flush() may be called for each dup'd fd, we don't  	// want to really close the file, we just want to flush. This  	// is achieved by closing a dup'd fd.  	newFd, err := syscall.Dup(int(f.fd.Fd())) -	f.lock.Unlock() +	f.fdLock.Unlock()  	if err != nil {  		return fuse.ToStatus(err) @@ -222,14 +303,27 @@ func (f *file) Flush() fuse.Status {  }  func (f *file) Fsync(flags int) (code fuse.Status) { -	f.lock.Lock() +	f.fdLock.Lock()  	r := fuse.ToStatus(syscall.Fsync(int(f.fd.Fd()))) -	f.lock.Unlock() +	f.fdLock.Unlock()  	return r  }  func (f *file) Truncate(newSize uint64) fuse.Status { +	// Common case first: Truncate to zero +	if newSize == 0 { +		f.fdLock.Lock() +		err := syscall.Ftruncate(int(f.fd.Fd()), 0) +		f.fdLock.Unlock() +		if err != nil { +			cryptfs.Warn.Printf("Ftruncate(fd, 0) returned error: %v", err) +			return fuse.ToStatus(err) +		} +		// A truncate to zero kills the file header +		f.header = nil +		return fuse.OK +	}  	// We need the old file size to determine if we are growing or shrinking  	// the file @@ -247,6 +341,15 @@ func (f *file) Truncate(newSize uint64) fuse.Status {  	// File grows  	if newSize > oldSize { + +		// File was empty, create new header +		if oldSize == 0 { +			err := f.createHeader() +			if err != nil { +				return fuse.ToStatus(err) +			} +		} +  		blocks := f.cfs.SplitRange(oldSize, newSize-oldSize)  		for _, b := range blocks {  			// First and last block may be partial @@ -259,9 +362,9 @@ func (f *file) Truncate(newSize uint64) fuse.Status {  				}  			} else {  				off, length := b.CiphertextRange() -				f.lock.Lock() +				f.fdLock.Lock()  				err := syscall.Ftruncate(int(f.fd.Fd()), int64(off+length)) -				f.lock.Unlock() +				f.fdLock.Unlock()  				if err != nil {  					cryptfs.Warn.Printf("grow Ftruncate returned error: %v", err)  					return fuse.ToStatus(err) @@ -272,7 +375,7 @@ func (f *file) Truncate(newSize uint64) fuse.Status {  	} else {  		// File shrinks  		blockNo := f.cfs.BlockNoPlainOff(newSize) -		cipherOff := blockNo * f.cfs.CipherBS() +		cipherOff := cryptfs.HEADER_LEN + blockNo * f.cfs.CipherBS()  		plainOff := blockNo * f.cfs.PlainBS()  		lastBlockLen := newSize - plainOff  		var data []byte @@ -284,13 +387,15 @@ func (f *file) Truncate(newSize uint64) fuse.Status {  				return status  			}  		} -		f.lock.Lock() +		// Truncate down to last complete block +		f.fdLock.Lock()  		err = syscall.Ftruncate(int(f.fd.Fd()), int64(cipherOff)) -		f.lock.Unlock() +		f.fdLock.Unlock()  		if err != nil {  			cryptfs.Warn.Printf("shrink Ftruncate returned error: %v", err)  			return fuse.ToStatus(err)  		} +		// Append partial block  		if lastBlockLen > 0 {  			_, status := f.doWrite(data, int64(plainOff))  			return status @@ -300,17 +405,17 @@ func (f *file) Truncate(newSize uint64) fuse.Status {  }  func (f *file) Chmod(mode uint32) fuse.Status { -	f.lock.Lock() +	f.fdLock.Lock()  	r := fuse.ToStatus(f.fd.Chmod(os.FileMode(mode))) -	f.lock.Unlock() +	f.fdLock.Unlock()  	return r  }  func (f *file) Chown(uid uint32, gid uint32) fuse.Status { -	f.lock.Lock() +	f.fdLock.Lock()  	r := fuse.ToStatus(f.fd.Chown(int(uid), int(gid))) -	f.lock.Unlock() +	f.fdLock.Unlock()  	return r  } @@ -318,9 +423,9 @@ func (f *file) Chown(uid uint32, gid uint32) fuse.Status {  func (f *file) GetAttr(a *fuse.Attr) fuse.Status {  	cryptfs.Debug.Printf("file.GetAttr()\n")  	st := syscall.Stat_t{} -	f.lock.Lock() +	f.fdLock.Lock()  	err := syscall.Fstat(int(f.fd.Fd()), &st) -	f.lock.Unlock() +	f.fdLock.Unlock()  	if err != nil {  		return fuse.ToStatus(err)  	} @@ -330,7 +435,7 @@ func (f *file) GetAttr(a *fuse.Attr) fuse.Status {  	return fuse.OK  } -// Allocate FUSE call, fallocate(2) +// Allocate - FUSE call, fallocate(2)  func (f *file) Allocate(off uint64, sz uint64, mode uint32) fuse.Status {  	cryptfs.Warn.Printf("Fallocate is not supported, returning ENOSYS - see https://github.com/rfjakob/gocryptfs/issues/1\n")  	return fuse.ENOSYS @@ -354,9 +459,9 @@ func (f *file) Utimens(a *time.Time, m *time.Time) fuse.Status {  		ts[1].Sec = m.Unix()  	} -	f.lock.Lock() +	f.fdLock.Lock()  	fn := fmt.Sprintf("/proc/self/fd/%d", f.fd.Fd())  	err := syscall.UtimesNano(fn, ts) -	f.lock.Unlock() +	f.fdLock.Unlock()  	return fuse.ToStatus(err)  } diff --git a/pathfs_frontend/fs.go b/pathfs_frontend/fs.go index ba46309..eebc87b 100644 --- a/pathfs_frontend/fs.go +++ b/pathfs_frontend/fs.go @@ -119,7 +119,8 @@ func (fs *FS) Mknod(name string, mode uint32, dev uint32, context *fuse.Context)  }  func (fs *FS) Truncate(path string, offset uint64, context *fuse.Context) (code fuse.Status) { -	return fs.FileSystem.Truncate(fs.EncryptPath(path), offset, context) +	cryptfs.Warn.Printf("Truncate of a closed file is not supported, returning ENOSYS\n") +	return fuse.ENOSYS  }  func (fs *FS) Utimens(path string, Atime *time.Time, Mtime *time.Time, context *fuse.Context) (code fuse.Status) { | 
