diff options
author | Jakob Unterwurzacher | 2015-11-01 01:32:33 +0100 |
---|---|---|
committer | Jakob Unterwurzacher | 2015-11-01 01:38:27 +0100 |
commit | 76311b60f2e208dbd93e1e7b6e9794770c14fede (patch) | |
tree | fa71d48744fd1ad6de26aaadd03f2440e4d4103d | |
parent | 73fa8efdb27172210b9751eb86689287db0b1170 (diff) |
Add file header (on-disk-format change)
Format: [ "Version" uint16 big endian ] [ "Id" 16 random bytes ]
Quoting SECURITY.md:
* 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 or copied between different files without
causing an decryption error.
-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) { |