diff options
Diffstat (limited to 'internal/contentenc')
| -rw-r--r-- | internal/contentenc/content.go | 116 | ||||
| -rw-r--r-- | internal/contentenc/content_api.go | 31 | ||||
| -rw-r--r-- | internal/contentenc/content_test.go | 91 | ||||
| -rw-r--r-- | internal/contentenc/file_header.go | 60 | ||||
| -rw-r--r-- | internal/contentenc/intrablock.go | 51 | ||||
| -rw-r--r-- | internal/contentenc/offsets.go | 97 | 
6 files changed, 446 insertions, 0 deletions
| diff --git a/internal/contentenc/content.go b/internal/contentenc/content.go new file mode 100644 index 0000000..14135a2 --- /dev/null +++ b/internal/contentenc/content.go @@ -0,0 +1,116 @@ +package contentenc + +// File content encryption / decryption + +import ( +	"encoding/binary" +	"bytes" +	"encoding/hex" +	"errors" + +	"github.com/rfjakob/gocryptfs/internal/toggledlog" +) + +// DecryptBlocks - Decrypt a number of blocks +func (be *ContentEnc) 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, fileId) +		if err != nil { +			break +		} +		pBuf.Write(pBlock) +		firstBlockNo++ +	} +	return pBuf.Bytes(), err +} + +// DecryptBlock - Verify and decrypt GCM block +// +// 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 *ContentEnc) DecryptBlock(ciphertext []byte, blockNo uint64, fileId []byte) ([]byte, error) { + +	// Empty block? +	if len(ciphertext) == 0 { +		return ciphertext, nil +	} + +	// All-zero block? +	if bytes.Equal(ciphertext, be.allZeroBlock) { +		toggledlog.Debug.Printf("DecryptBlock: file hole encountered") +		return make([]byte, be.plainBS), nil +	} + +	if len(ciphertext) < be.cryptoCore.IVLen { +		toggledlog.Warn.Printf("DecryptBlock: Block is too short: %d bytes", len(ciphertext)) +		return nil, errors.New("Block is too short") +	} + +	// Extract nonce +	nonce := ciphertext[:be.cryptoCore.IVLen] +	ciphertextOrig := ciphertext +	ciphertext = ciphertext[be.cryptoCore.IVLen:] + +	// Decrypt +	var plaintext []byte +	aData := make([]byte, 8) +	aData = append(aData, fileId...) +	binary.BigEndian.PutUint64(aData, blockNo) +	plaintext, err := be.cryptoCore.Gcm.Open(plaintext, nonce, ciphertext, aData) + +	if err != nil { +		toggledlog.Warn.Printf("DecryptBlock: %s, len=%d", err.Error(), len(ciphertextOrig)) +		toggledlog.Debug.Println(hex.Dump(ciphertextOrig)) +		return nil, err +	} + +	return plaintext, nil +} + +// encryptBlock - Encrypt and add IV and MAC +func (be *ContentEnc) EncryptBlock(plaintext []byte, blockNo uint64, fileID []byte) []byte { + +	// Empty block? +	if len(plaintext) == 0 { +		return plaintext +	} + +	// Get fresh nonce +	nonce := be.cryptoCore.GcmIVGen.Get() + +	// Authenticate block with block number and file ID +	aData := make([]byte, 8) +	binary.BigEndian.PutUint64(aData, blockNo) +	aData = append(aData, fileID...) + +	// Encrypt plaintext and append to nonce +	ciphertext := be.cryptoCore.Gcm.Seal(nonce, nonce, plaintext, aData) + +	return ciphertext +} + +// MergeBlocks - Merge newData into oldData at offset +// New block may be bigger than both newData and oldData +func (be *ContentEnc) MergeBlocks(oldData []byte, newData []byte, offset int) []byte { + +	// Make block of maximum size +	out := make([]byte, be.plainBS) + +	// Copy old and new data into it +	copy(out, oldData) +	l := len(newData) +	copy(out[offset:offset+l], newData) + +	// Crop to length +	outLen := len(oldData) +	newLen := offset + len(newData) +	if outLen < newLen { +		outLen = newLen +	} +	return out[0:outLen] +} diff --git a/internal/contentenc/content_api.go b/internal/contentenc/content_api.go new file mode 100644 index 0000000..1700d35 --- /dev/null +++ b/internal/contentenc/content_api.go @@ -0,0 +1,31 @@ +package contentenc + +import "github.com/rfjakob/gocryptfs/internal/cryptocore" + +type ContentEnc struct { +	// Cryptographic primitives +	cryptoCore *cryptocore.CryptoCore +	// Plaintext block size +	plainBS     uint64 +	// Ciphertext block size +	cipherBS    uint64 +	// All-zero block of size cipherBS, for fast compares +	allZeroBlock []byte +} + +func New(cc *cryptocore.CryptoCore, plainBS uint64) *ContentEnc { + +	cipherBS := plainBS + uint64(cc.IVLen) + cryptocore.AuthTagLen + +	return &ContentEnc{ +		cryptoCore: cc, +		plainBS: plainBS, +		cipherBS: cipherBS, +		allZeroBlock: make([]byte, cipherBS), +	} +} + + +func (be *ContentEnc) PlainBS() uint64 { +	return be.plainBS +} diff --git a/internal/contentenc/content_test.go b/internal/contentenc/content_test.go new file mode 100644 index 0000000..70ad58d --- /dev/null +++ b/internal/contentenc/content_test.go @@ -0,0 +1,91 @@ +package contentenc + +import ( +	"testing" +) + +type testRange struct { +	offset uint64 +	length uint64 +} + +func TestSplitRange(t *testing.T) { +	var ranges []testRange + +	ranges = append(ranges, testRange{0, 70000}, +		testRange{0, 10}, +		testRange{234, 6511}, +		testRange{65444, 54}, +		testRange{0, 1024 * 1024}, +		testRange{0, 65536}, +		testRange{6654, 8945}) + +	key := make([]byte, KEY_LEN) +	f := NewCryptFS(key, true, false, true) + +	for _, r := range ranges { +		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) +			} +			lastBlockNo = p.BlockNo +			if p.Length > DEFAULT_PLAINBS || p.Skip >= DEFAULT_PLAINBS { +				t.Errorf("Test fail: n=%d, length=%d, offset=%d\n", p.BlockNo, p.Length, p.Skip) +			} +		} +	} +} + +func TestCiphertextRange(t *testing.T) { +	var ranges []testRange + +	ranges = append(ranges, testRange{0, 70000}, +		testRange{0, 10}, +		testRange{234, 6511}, +		testRange{65444, 54}, +		testRange{6654, 8945}) + +	key := make([]byte, KEY_LEN) +	f := NewCryptFS(key, true, false, true) + +	for _, r := range ranges { + +		blocks := f.ExplodePlainRange(r.offset, r.length) +		alignedOffset, alignedLength := blocks[0].JointCiphertextRange(blocks) +		skipBytes := blocks[0].Skip + +		if alignedLength < r.length { +			t.Errorf("alignedLength=%d is smaller than length=%d", alignedLength, r.length) +		} +		if (alignedOffset-HEADER_LEN)%f.cipherBS != 0 { +			t.Errorf("alignedOffset=%d is not aligned", alignedOffset) +		} +		if r.offset%f.plainBS != 0 && skipBytes == 0 { +			t.Errorf("skipBytes=0") +		} +	} +} + +func TestBlockNo(t *testing.T) { +	key := make([]byte, KEY_LEN) +	f := NewCryptFS(key, true, false, true) + +	b := f.CipherOffToBlockNo(788) +	if b != 0 { +		t.Errorf("actual: %d", b) +	} +	b = f.CipherOffToBlockNo(HEADER_LEN + f.cipherBS) +	if b != 1 { +		t.Errorf("actual: %d", b) +	} +	b = f.PlainOffToBlockNo(788) +	if b != 0 { +		t.Errorf("actual: %d", b) +	} +	b = f.PlainOffToBlockNo(f.plainBS) +	if b != 1 { +		t.Errorf("actual: %d", b) +	} +} diff --git a/internal/contentenc/file_header.go b/internal/contentenc/file_header.go new file mode 100644 index 0000000..8a9dd2c --- /dev/null +++ b/internal/contentenc/file_header.go @@ -0,0 +1,60 @@ +package contentenc + +// Per-file header +// +// Format: [ "Version" uint16 big endian ] [ "Id" 16 random bytes ] + +import ( +	"encoding/binary" +	"fmt" + +	"github.com/rfjakob/gocryptfs/internal/cryptocore" +) + +const ( +	// Current On-Disk-Format version +	CurrentVersion = 2 + +	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 != CurrentVersion { +		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 != CurrentVersion { +		return nil, fmt.Errorf("ParseHeader: invalid version: got %d, want %d", h.Version, CurrentVersion) +	} +	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 = CurrentVersion +	h.Id = cryptocore.RandBytes(HEADER_ID_LEN) +	return &h +} diff --git a/internal/contentenc/intrablock.go b/internal/contentenc/intrablock.go new file mode 100644 index 0000000..330b980 --- /dev/null +++ b/internal/contentenc/intrablock.go @@ -0,0 +1,51 @@ +package contentenc + +// intraBlock identifies a part of a file block +type intraBlock struct { +	BlockNo uint64 // Block number in file +	Skip    uint64 // Offset into block plaintext +	Length  uint64 // Length of data from this block +	fs      *ContentEnc +} + +// isPartial - is the block partial? This means we have to do read-modify-write. +func (ib *intraBlock) IsPartial() bool { +	if ib.Skip > 0 || ib.Length < ib.fs.plainBS { +		return true +	} +	return false +} + +// CiphertextRange - get byte range in ciphertext file corresponding to BlockNo +// (complete block) +func (ib *intraBlock) CiphertextRange() (offset uint64, length uint64) { +	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.fs.BlockNoToPlainOff(ib.BlockNo), ib.fs.plainBS +} + +// CropBlock - crop a potentially larger plaintext block down to the relevant part +func (ib *intraBlock) CropBlock(d []byte) []byte { +	lenHave := len(d) +	lenWant := int(ib.Skip + ib.Length) +	if lenHave < lenWant { +		return d[ib.Skip:lenHave] +	} +	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/internal/contentenc/offsets.go b/internal/contentenc/offsets.go new file mode 100644 index 0000000..1b5952f --- /dev/null +++ b/internal/contentenc/offsets.go @@ -0,0 +1,97 @@ +package contentenc + +import ( +	"github.com/rfjakob/gocryptfs/internal/toggledlog" +) + +// Contentenc methods that translate offsets between ciphertext and plaintext + +// get the block number at plain-text offset +func (be *ContentEnc) PlainOffToBlockNo(plainOffset uint64) uint64 { +	return plainOffset / be.plainBS +} + +// get the block number at ciphter-text offset +func (be *ContentEnc) CipherOffToBlockNo(cipherOffset uint64) uint64 { +	return (cipherOffset - HEADER_LEN) / be.cipherBS +} + +// get ciphertext offset of block "blockNo" +func (be *ContentEnc) BlockNoToCipherOff(blockNo uint64) uint64 { +	return HEADER_LEN + blockNo*be.cipherBS +} + +// get plaintext offset of block "blockNo" +func (be *ContentEnc) BlockNoToPlainOff(blockNo uint64) uint64 { +	return blockNo * be.plainBS +} + +// PlainSize - calculate plaintext size from ciphertext size +func (be *ContentEnc) CipherSizeToPlainSize(cipherSize uint64) uint64 { + +	// Zero sized files stay zero-sized +	if cipherSize == 0 { +		return 0 +	} + +	if cipherSize == HEADER_LEN { +		toggledlog.Warn.Printf("cipherSize %d == header size: interrupted write?\n", cipherSize) +		return 0 +	} + +	if cipherSize < HEADER_LEN { +		toggledlog.Warn.Printf("cipherSize %d < header size: corrupt file\n", cipherSize) +		return 0 +	} + +	// Block number at last byte +	blockNo := be.CipherOffToBlockNo(cipherSize - 1) +	blockCount := blockNo + 1 + +	overhead := be.BlockOverhead()*blockCount + HEADER_LEN + +	return cipherSize - overhead +} + +// CipherSize - calculate ciphertext size from plaintext size +func (be *ContentEnc) PlainSizeToCipherSize(plainSize uint64) uint64 { + +	// Block number at last byte +	blockNo := be.PlainOffToBlockNo(plainSize - 1) +	blockCount := blockNo + 1 + +	overhead := be.BlockOverhead()*blockCount + HEADER_LEN + +	return plainSize + overhead +} + +// Split a plaintext byte range into (possibly partial) blocks +func (be *ContentEnc) 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 (be *ContentEnc) BlockOverhead() uint64 { +	return be.cipherBS - be.plainBS +} + +func MinUint64(x uint64, y uint64) uint64 { +	if x < y { +		return x +	} +	return y +} | 
