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.
This commit is contained in:
Jakob Unterwurzacher 2015-11-01 01:32:33 +01:00
parent 73fa8efdb2
commit 76311b60f2
10 changed files with 260 additions and 61 deletions

View File

@ -62,3 +62,19 @@ The output should look like this:
BenchmarkStreamRead 200 7848155 ns/op 133.61 MB/s BenchmarkStreamRead 200 7848155 ns/op 133.61 MB/s
ok github.com/rfjakob/gocryptfs 9.407s 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.

View File

@ -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 * 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 the block and decryption will be aborted. The failure is logged and an
I/O error is returned to the user. 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 * This means the position of the blocks is protected as well. The blocks
can not be reordered without causing an decryption error. can not be reordered or copied between different files without
* However, proper affiliation of a block to the file is can not be verified. causing an decryption error.
* This means that blocks can be copied between different files provided
that they stay at the same position.
* For technical reasons (sparse files), the special "all-zero" block is * For technical reasons (sparse files), the special "all-zero" block is
always seen as a valid block that decrypts to all-zero plaintext. always seen as a valid block that decrypts to all-zero plaintext.
* This means that whole blocks can be zeroed out * This means that whole blocks can be zeroed out

View File

@ -1,6 +1,7 @@
package cryptfs package cryptfs
import ( import (
"fmt"
"encoding/json" "encoding/json"
"io/ioutil" "io/ioutil"
) )
@ -19,6 +20,8 @@ type ConfFile struct {
EncryptedKey []byte EncryptedKey []byte
// Stores parameters for scrypt hashing (key derivation) // Stores parameters for scrypt hashing (key derivation)
ScryptObject scryptKdf ScryptObject scryptKdf
// The On-Disk-Format version this filesystem uses
Version uint16
} }
// CreateConfFile - create a new config with a random key encrypted with // 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) key := RandBytes(KEY_LEN)
// Encrypt it using the password // Encrypt it using the password
// This sets ScryptObject and EncryptedKey
cf.EncryptKey(key, password) cf.EncryptKey(key, password)
cf.Version = HEADER_CURRENT_VERSION
// Write file to disk // Write file to disk
err := cf.WriteFile() err := cf.WriteFile()
@ -58,6 +64,10 @@ func LoadConfFile(filename string, password string) ([]byte, *ConfFile, error) {
return nil, nil, err 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 // Generate derived key from password
scryptHash := cf.ScryptObject.DeriveKey(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 // We use stock go GCM instead of OpenSSL here as speed is not important
// and we get better error messages // and we get better error messages
cfs := NewCryptFS(scryptHash, false) cfs := NewCryptFS(scryptHash, false)
key, err := cfs.DecryptBlock(cf.EncryptedKey, 0) key, err := cfs.DecryptBlock(cf.EncryptedKey, 0, nil)
if err != nil { if err != nil {
Warn.Printf("failed to unlock master key: %s\n", err.Error()) Warn.Printf("failed to unlock master key: %s\n", err.Error())
Warn.Printf("Password incorrect.\n") Warn.Printf("Password incorrect.\n")
@ -84,7 +94,7 @@ func (cf *ConfFile) EncryptKey(key []byte, password string) {
// Lock master key using password-based key // Lock master key using password-based key
cfs := NewCryptFS(scryptHash, false) 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" // WriteFile - write out config in JSON format to file "filename.tmp"

View File

@ -1,7 +1,6 @@
package cryptfs package cryptfs
import ( import (
"fmt"
"testing" "testing"
) )
@ -17,6 +16,8 @@ func TestSplitRange(t *testing.T) {
testRange{0, 10}, testRange{0, 10},
testRange{234, 6511}, testRange{234, 6511},
testRange{65444, 54}, testRange{65444, 54},
testRange{0, 1024*1024},
testRange{0, 65536},
testRange{6654, 8945}) testRange{6654, 8945})
key := make([]byte, KEY_LEN) key := make([]byte, KEY_LEN)
@ -24,10 +25,14 @@ func TestSplitRange(t *testing.T) {
for _, r := range ranges { for _, r := range ranges {
parts := f.SplitRange(r.offset, r.length) parts := f.SplitRange(r.offset, r.length)
var lastBlockNo uint64 = 1<<63
for _, p := range parts { 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 { 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.Errorf("Test fail: n=%d, length=%d, offset=%d\n", p.BlockNo, p.Length, p.Skip)
t.Fail()
} }
} }
} }
@ -48,13 +53,13 @@ func TestCiphertextRange(t *testing.T) {
for _, r := range ranges { for _, r := range ranges {
alignedOffset, alignedLength, skipBytes := f.CiphertextRange(r.offset, r.length) alignedOffset, alignedLength, skipBytes := f.CiphertextRange(r.offset, r.length)
if alignedLength < 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 { if (alignedOffset - HEADER_LEN)%f.cipherBS != 0 {
t.Fail() t.Errorf("alignedOffset=%d is not aligned", alignedOffset)
} }
if r.offset%f.plainBS != 0 && skipBytes == 0 { 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 { if b != 0 {
t.Errorf("actual: %d", b) t.Errorf("actual: %d", b)
} }
b = f.BlockNoCipherOff(f.CipherBS()) b = f.BlockNoCipherOff(HEADER_LEN + f.CipherBS())
if b != 1 { if b != 1 {
t.Errorf("actual: %d", b) t.Errorf("actual: %d", b)
} }

View File

@ -13,6 +13,7 @@ const (
KEY_LEN = 32 // AES-256 KEY_LEN = 32 // AES-256
NONCE_LEN = 12 NONCE_LEN = 12
AUTH_TAG_LEN = 16 AUTH_TAG_LEN = 16
BLOCK_OVERHEAD = NONCE_LEN + AUTH_TAG_LEN
) )
type CryptFS struct { type CryptFS struct {

View File

@ -30,14 +30,14 @@ type CryptFile struct {
} }
// DecryptBlocks - Decrypt a number of blocks // 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) cBuf := bytes.NewBuffer(ciphertext)
var err error var err error
var pBuf bytes.Buffer var pBuf bytes.Buffer
for cBuf.Len() > 0 { for cBuf.Len() > 0 {
cBlock := cBuf.Next(int(be.cipherBS)) cBlock := cBuf.Next(int(be.cipherBS))
var pBlock []byte var pBlock []byte
pBlock, err = be.DecryptBlock(cBlock, firstBlockNo) pBlock, err = be.DecryptBlock(cBlock, firstBlockNo, fileId)
if err != nil { if err != nil {
break 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 // Corner case: A full-sized block of all-zero ciphertext bytes is translated
// to an all-zero plaintext block, i.e. file hole passtrough. // 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? // Empty block?
if len(ciphertext) == 0 { if len(ciphertext) == 0 {
@ -77,6 +77,7 @@ func (be *CryptFS) DecryptBlock(ciphertext []byte, blockNo uint64) ([]byte, erro
// Decrypt // Decrypt
var plaintext []byte var plaintext []byte
aData := make([]byte, 8) aData := make([]byte, 8)
aData = append(aData, fileId...)
binary.BigEndian.PutUint64(aData, blockNo) binary.BigEndian.PutUint64(aData, blockNo)
plaintext, err := be.gcm.Open(plaintext, nonce, ciphertext, aData) 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 return plaintext, nil
} }
// encryptBlock - Encrypt and add MAC using GCM // encryptBlock - Encrypt and add IV and MAC
func (be *CryptFS) EncryptBlock(plaintext []byte, blockNo uint64) []byte { func (be *CryptFS) EncryptBlock(plaintext []byte, blockNo uint64, fileId []byte) []byte {
// Empty block? // Empty block?
if len(plaintext) == 0 { if len(plaintext) == 0 {
@ -103,6 +104,7 @@ func (be *CryptFS) EncryptBlock(plaintext []byte, blockNo uint64) []byte {
// Encrypt plaintext and append to nonce // Encrypt plaintext and append to nonce
aData := make([]byte, 8) aData := make([]byte, 8)
binary.BigEndian.PutUint64(aData, blockNo) binary.BigEndian.PutUint64(aData, blockNo)
aData = append(aData, fileId...)
ciphertext := be.gcm.Seal(nonce, nonce, plaintext, aData) ciphertext := be.gcm.Seal(nonce, nonce, plaintext, aData)
return ciphertext return ciphertext
@ -118,6 +120,7 @@ func (be *CryptFS) SplitRange(offset uint64, length uint64) []intraBlock {
for length > 0 { for length > 0 {
b.BlockNo = offset / be.plainBS b.BlockNo = offset / be.plainBS
b.Skip = 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) b.Length = be.minu64(length, be.plainBS-b.Skip)
parts = append(parts, b) parts = append(parts, b)
offset += b.Length offset += b.Length
@ -134,6 +137,9 @@ func (be *CryptFS) PlainSize(size uint64) uint64 {
return 0 return 0
} }
// Account for header
size -= HEADER_LEN
overhead := be.cipherBS - be.plainBS overhead := be.cipherBS - be.plainBS
nBlocks := (size + be.cipherBS - 1) / be.cipherBS nBlocks := (size + be.cipherBS - 1) / be.cipherBS
if nBlocks*overhead > size { if nBlocks*overhead > size {
@ -171,7 +177,7 @@ func (be *CryptFS) CiphertextRange(offset uint64, length uint64) (alignedOffset
firstBlockNo := offset / be.plainBS firstBlockNo := offset / be.plainBS
lastBlockNo := (offset + length - 1) / be.plainBS lastBlockNo := (offset + length - 1) / be.plainBS
alignedOffset = firstBlockNo * be.cipherBS alignedOffset = HEADER_LEN + firstBlockNo * be.cipherBS
alignedLength = (lastBlockNo - firstBlockNo + 1) * be.cipherBS alignedLength = (lastBlockNo - firstBlockNo + 1) * be.cipherBS
skipBytes = int(skip) skipBytes = int(skip)
@ -232,5 +238,5 @@ func (be *CryptFS) BlockNoPlainOff(plainOffset uint64) uint64 {
// Get the block number at ciphter-text offset // Get the block number at ciphter-text offset
func (be *CryptFS) BlockNoCipherOff(cipherOffset uint64) uint64 { func (be *CryptFS) BlockNoCipherOff(cipherOffset uint64) uint64 {
return cipherOffset / be.cipherBS return (cipherOffset - HEADER_LEN) / be.cipherBS
} }

56
cryptfs/file_header.go Normal file
View File

@ -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
}

View File

@ -19,7 +19,7 @@ func (ib *intraBlock) IsPartial() bool {
// CiphertextRange - get byte range in ciphertext file corresponding to BlockNo // CiphertextRange - get byte range in ciphertext file corresponding to BlockNo
// (complete block) // (complete block)
func (ib *intraBlock) CiphertextRange() (offset uint64, length uint64) { 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 // PlaintextRange - get byte range in plaintext corresponding to BlockNo

View File

@ -23,7 +23,7 @@ type file struct {
// reuse the fd number after it is closed. When open races // reuse the fd number after it is closed. When open races
// with another close, they may lead to confusion as which // with another close, they may lead to confusion as which
// file gets written in the end. // file gets written in the end.
lock sync.Mutex fdLock sync.Mutex
// Was the file opened O_WRONLY? // Was the file opened O_WRONLY?
writeOnly bool writeOnly bool
@ -33,6 +33,9 @@ type file struct {
// Inode number // Inode number
ino uint64 ino uint64
// File header
header *cryptfs.FileHeader
} }
func NewFile(fd *os.File, writeOnly bool, cfs *cryptfs.CryptFS) nodefs.File { 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) { 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 { func (f *file) String() string {
return fmt.Sprintf("cryptFile(%s)", f.fd.Name()) return fmt.Sprintf("cryptFile(%s)", f.fd.Name())
} }
// doRead - returns "length" plaintext bytes from plaintext offset "off". // 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. // 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) { 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 // Read the backing ciphertext in one go
alignedOffset, alignedLength, skip := f.cfs.CiphertextRange(off, length) alignedOffset, alignedLength, skip := f.cfs.CiphertextRange(off, length)
cryptfs.Debug.Printf("CiphertextRange(%d, %d) -> %d, %d, %d\n", off, length, alignedOffset, alignedLength, skip) cryptfs.Debug.Printf("CiphertextRange(%d, %d) -> %d, %d, %d\n", off, length, alignedOffset, alignedLength, skip)
ciphertext := make([]byte, int(alignedLength)) ciphertext := make([]byte, int(alignedLength))
f.lock.Lock() f.fdLock.Lock()
n, err := f.fd.ReadAt(ciphertext, int64(alignedOffset)) n, err := f.fd.ReadAt(ciphertext, int64(alignedOffset))
f.lock.Unlock() f.fdLock.Unlock()
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
cryptfs.Warn.Printf("read: ReadAt: %s\n", err.Error()) cryptfs.Warn.Printf("read: ReadAt: %s\n", err.Error())
return nil, fuse.ToStatus(err) 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 // Truncate ciphertext buffer down to actually read bytes
ciphertext = ciphertext[0:n] 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) cryptfs.Debug.Printf("ReadAt offset=%d bytes (%d blocks), want=%d, got=%d\n", alignedOffset, blockNo, alignedLength, n)
// Decrypt it // Decrypt it
plaintext, err := f.cfs.DecryptBlocks(ciphertext, blockNo) plaintext, err := f.cfs.DecryptBlocks(ciphertext, blockNo, f.header.Id)
if err != nil { if err != nil {
blockNo := (alignedOffset + uint64(len(plaintext))) / f.cfs.PlainBS() 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() plainOff := blockNo * f.cfs.PlainBS()
cryptfs.Warn.Printf("ino%d: doRead: corrupt block #%d (plainOff=%d/%d, cipherOff=%d/%d)\n", 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()) 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)] out = plaintext[skip : skip+int(length)]
} else if lenHave > skip { } else if lenHave > skip {
out = plaintext[skip:lenHave] 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 return out, fuse.OK
} }
// Read - FUSE call // Read - FUSE call
func (f *file) Read(buf []byte, off int64) (resultData fuse.ReadResult, code fuse.Status) { 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) cryptfs.Debug.Printf("ino%d: FUSE Read: offset=%d length=%d\n", f.ino, len(buf), off)
if f.writeOnly { 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 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) { 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 var written uint32
status := fuse.OK status := fuse.OK
dataBuf := bytes.NewBuffer(data) dataBuf := bytes.NewBuffer(data)
@ -158,14 +237,16 @@ func (f *file) doWrite(data []byte, off int64) (uint32, fuse.Status) {
// Write // Write
blockOffset, _ := b.CiphertextRange() blockOffset, _ := b.CiphertextRange()
blockData = f.cfs.EncryptBlock(blockData, b.BlockNo) 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), b.BlockNo, cryptfs.Debug.Md5sum(blockData)) 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()) { 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)) _, err := f.fd.WriteAt(blockData, int64(blockOffset))
f.lock.Unlock() f.fdLock.Unlock()
if err != nil { if err != nil {
cryptfs.Warn.Printf("Write failed: %s\n", err.Error()) 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 // Release - FUSE call, forget file
func (f *file) Release() { func (f *file) Release() {
f.lock.Lock() f.fdLock.Lock()
f.fd.Close() f.fd.Close()
f.lock.Unlock() f.fdLock.Unlock()
} }
// Flush - FUSE call // Flush - FUSE call
func (f *file) Flush() fuse.Status { 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 // 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 // want to really close the file, we just want to flush. This
// is achieved by closing a dup'd fd. // is achieved by closing a dup'd fd.
newFd, err := syscall.Dup(int(f.fd.Fd())) newFd, err := syscall.Dup(int(f.fd.Fd()))
f.lock.Unlock() f.fdLock.Unlock()
if err != nil { if err != nil {
return fuse.ToStatus(err) return fuse.ToStatus(err)
@ -222,14 +303,27 @@ func (f *file) Flush() fuse.Status {
} }
func (f *file) Fsync(flags int) (code 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()))) r := fuse.ToStatus(syscall.Fsync(int(f.fd.Fd())))
f.lock.Unlock() f.fdLock.Unlock()
return r return r
} }
func (f *file) Truncate(newSize uint64) fuse.Status { 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 // We need the old file size to determine if we are growing or shrinking
// the file // the file
@ -247,6 +341,15 @@ func (f *file) Truncate(newSize uint64) fuse.Status {
// File grows // File grows
if newSize > oldSize { 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) blocks := f.cfs.SplitRange(oldSize, newSize-oldSize)
for _, b := range blocks { for _, b := range blocks {
// First and last block may be partial // First and last block may be partial
@ -259,9 +362,9 @@ func (f *file) Truncate(newSize uint64) fuse.Status {
} }
} else { } else {
off, length := b.CiphertextRange() off, length := b.CiphertextRange()
f.lock.Lock() f.fdLock.Lock()
err := syscall.Ftruncate(int(f.fd.Fd()), int64(off+length)) err := syscall.Ftruncate(int(f.fd.Fd()), int64(off+length))
f.lock.Unlock() f.fdLock.Unlock()
if err != nil { if err != nil {
cryptfs.Warn.Printf("grow Ftruncate returned error: %v", err) cryptfs.Warn.Printf("grow Ftruncate returned error: %v", err)
return fuse.ToStatus(err) return fuse.ToStatus(err)
@ -272,7 +375,7 @@ func (f *file) Truncate(newSize uint64) fuse.Status {
} else { } else {
// File shrinks // File shrinks
blockNo := f.cfs.BlockNoPlainOff(newSize) blockNo := f.cfs.BlockNoPlainOff(newSize)
cipherOff := blockNo * f.cfs.CipherBS() cipherOff := cryptfs.HEADER_LEN + blockNo * f.cfs.CipherBS()
plainOff := blockNo * f.cfs.PlainBS() plainOff := blockNo * f.cfs.PlainBS()
lastBlockLen := newSize - plainOff lastBlockLen := newSize - plainOff
var data []byte var data []byte
@ -284,13 +387,15 @@ func (f *file) Truncate(newSize uint64) fuse.Status {
return status return status
} }
} }
f.lock.Lock() // Truncate down to last complete block
f.fdLock.Lock()
err = syscall.Ftruncate(int(f.fd.Fd()), int64(cipherOff)) err = syscall.Ftruncate(int(f.fd.Fd()), int64(cipherOff))
f.lock.Unlock() f.fdLock.Unlock()
if err != nil { if err != nil {
cryptfs.Warn.Printf("shrink Ftruncate returned error: %v", err) cryptfs.Warn.Printf("shrink Ftruncate returned error: %v", err)
return fuse.ToStatus(err) return fuse.ToStatus(err)
} }
// Append partial block
if lastBlockLen > 0 { if lastBlockLen > 0 {
_, status := f.doWrite(data, int64(plainOff)) _, status := f.doWrite(data, int64(plainOff))
return status return status
@ -300,17 +405,17 @@ func (f *file) Truncate(newSize uint64) fuse.Status {
} }
func (f *file) Chmod(mode uint32) 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))) r := fuse.ToStatus(f.fd.Chmod(os.FileMode(mode)))
f.lock.Unlock() f.fdLock.Unlock()
return r return r
} }
func (f *file) Chown(uid uint32, gid uint32) fuse.Status { 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))) r := fuse.ToStatus(f.fd.Chown(int(uid), int(gid)))
f.lock.Unlock() f.fdLock.Unlock()
return r 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 { func (f *file) GetAttr(a *fuse.Attr) fuse.Status {
cryptfs.Debug.Printf("file.GetAttr()\n") cryptfs.Debug.Printf("file.GetAttr()\n")
st := syscall.Stat_t{} st := syscall.Stat_t{}
f.lock.Lock() f.fdLock.Lock()
err := syscall.Fstat(int(f.fd.Fd()), &st) err := syscall.Fstat(int(f.fd.Fd()), &st)
f.lock.Unlock() f.fdLock.Unlock()
if err != nil { if err != nil {
return fuse.ToStatus(err) return fuse.ToStatus(err)
} }
@ -330,7 +435,7 @@ func (f *file) GetAttr(a *fuse.Attr) fuse.Status {
return fuse.OK 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 { 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") cryptfs.Warn.Printf("Fallocate is not supported, returning ENOSYS - see https://github.com/rfjakob/gocryptfs/issues/1\n")
return fuse.ENOSYS return fuse.ENOSYS
@ -354,9 +459,9 @@ func (f *file) Utimens(a *time.Time, m *time.Time) fuse.Status {
ts[1].Sec = m.Unix() ts[1].Sec = m.Unix()
} }
f.lock.Lock() f.fdLock.Lock()
fn := fmt.Sprintf("/proc/self/fd/%d", f.fd.Fd()) fn := fmt.Sprintf("/proc/self/fd/%d", f.fd.Fd())
err := syscall.UtimesNano(fn, ts) err := syscall.UtimesNano(fn, ts)
f.lock.Unlock() f.fdLock.Unlock()
return fuse.ToStatus(err) return fuse.ToStatus(err)
} }

View File

@ -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) { 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) { func (fs *FS) Utimens(path string, Atime *time.Time, Mtime *time.Time, context *fuse.Context) (code fuse.Status) {