Refactor ciphertext <-> plaintext offset translation functions

Move all the intelligence into the new file address_translation.go.
That the calculations were spread out too much became apparent when adding
the file header. This should make the code much easier to modify in the
future.
This commit is contained in:
Jakob Unterwurzacher 2015-11-01 12:11:36 +01:00
parent 14276c9632
commit 902babdf22
12 changed files with 158 additions and 186 deletions

View File

@ -0,0 +1,79 @@
package cryptfs
// CryptFS methods that translate offsets between ciphertext and plaintext
// get the block number at plain-text offset
func (be *CryptFS) PlainOffToBlockNo(plainOffset uint64) uint64 {
return plainOffset / be.plainBS
}
// get the block number at ciphter-text offset
func (be *CryptFS) CipherOffToBlockNo(cipherOffset uint64) uint64 {
return (cipherOffset - HEADER_LEN) / be.cipherBS
}
// get ciphertext offset of block "blockNo"
func (be *CryptFS) BlockNoToCipherOff(blockNo uint64) uint64 {
return HEADER_LEN + blockNo*be.cipherBS
}
// get plaintext offset of block "blockNo"
func (be *CryptFS) BlockNoToPlainOff(blockNo uint64) uint64 {
return blockNo * be.plainBS
}
// PlainSize - calculate plaintext size from ciphertext size
func (be *CryptFS) CipherSizeToPlainSize(cipherSize uint64) uint64 {
// Zero sized files stay zero-sized
if cipherSize == 0 {
return 0
}
// Block number at last byte
blockNo := be.CipherOffToBlockNo(cipherSize - 1)
blockCount := blockNo + 1
overhead := BLOCK_OVERHEAD*blockCount + HEADER_LEN
return cipherSize - overhead
}
// CipherSize - calculate ciphertext size from plaintext size
func (be *CryptFS) PlainSizeToCipherSize(plainSize uint64) uint64 {
// Block number at last byte
blockNo := be.PlainOffToBlockNo(plainSize - 1)
blockCount := blockNo + 1
overhead := BLOCK_OVERHEAD*blockCount + HEADER_LEN
return plainSize + overhead
}
// Split a plaintext byte range into (possibly partial) blocks
func (be *CryptFS) ExplodePlainRange(offset uint64, length uint64) []intraBlock {
var blocks []intraBlock
var nextBlock intraBlock
nextBlock.fs = be
for length > 0 {
nextBlock.BlockNo = be.PlainOffToBlockNo(offset)
nextBlock.Skip = offset - be.BlockNoToPlainOff(nextBlock.BlockNo)
// Minimum of remaining data and remaining space in the block
nextBlock.Length = MinUint64(length, be.plainBS-nextBlock.Skip)
blocks = append(blocks, nextBlock)
offset += nextBlock.Length
length -= nextBlock.Length
}
return blocks
}
func MinUint64(x uint64, y uint64) uint64 {
if x < y {
return x
}
return y
}

View File

@ -1,8 +1,8 @@
package cryptfs
import (
"fmt"
"encoding/json"
"fmt"
"io/ioutil"
)
import "os"

View File

@ -16,7 +16,7 @@ func TestSplitRange(t *testing.T) {
testRange{0, 10},
testRange{234, 6511},
testRange{65444, 54},
testRange{0, 1024*1024},
testRange{0, 1024 * 1024},
testRange{0, 65536},
testRange{6654, 8945})
@ -24,8 +24,8 @@ func TestSplitRange(t *testing.T) {
f := NewCryptFS(key, true)
for _, r := range ranges {
parts := f.SplitRange(r.offset, r.length)
var lastBlockNo uint64 = 1<<63
parts := f.ExplodePlainRange(r.offset, r.length)
var lastBlockNo uint64 = 1 << 63
for _, p := range parts {
if p.BlockNo == lastBlockNo {
t.Errorf("Duplicate block number %d", p.BlockNo)
@ -51,11 +51,15 @@ func TestCiphertextRange(t *testing.T) {
f := NewCryptFS(key, true)
for _, r := range ranges {
alignedOffset, alignedLength, skipBytes := f.CiphertextRange(r.offset, r.length)
blocks := f.ExplodePlainRange(r.offset, r.length)
alignedOffset, alignedLength := blocks[0].JointCiphertextRange(blocks)
skipBytes := blocks[0].Skip
if alignedLength < r.length {
t.Errorf("alignedLength=%s is smaller than length=%d", alignedLength, r.length)
}
if (alignedOffset - HEADER_LEN)%f.cipherBS != 0 {
if (alignedOffset-HEADER_LEN)%f.cipherBS != 0 {
t.Errorf("alignedOffset=%d is not aligned", alignedOffset)
}
if r.offset%f.plainBS != 0 && skipBytes == 0 {
@ -68,19 +72,19 @@ func TestBlockNo(t *testing.T) {
key := make([]byte, KEY_LEN)
f := NewCryptFS(key, true)
b := f.BlockNoCipherOff(788)
b := f.CipherOffToBlockNo(788)
if b != 0 {
t.Errorf("actual: %d", b)
}
b = f.BlockNoCipherOff(HEADER_LEN + f.CipherBS())
b = f.CipherOffToBlockNo(HEADER_LEN + f.cipherBS)
if b != 1 {
t.Errorf("actual: %d", b)
}
b = f.BlockNoPlainOff(788)
b = f.PlainOffToBlockNo(788)
if b != 0 {
t.Errorf("actual: %d", b)
}
b = f.BlockNoPlainOff(f.PlainBS())
b = f.PlainOffToBlockNo(f.plainBS)
if b != 1 {
t.Errorf("actual: %d", b)
}

View File

@ -13,7 +13,7 @@ const (
KEY_LEN = 32 // AES-256
NONCE_LEN = 12
AUTH_TAG_LEN = 16
BLOCK_OVERHEAD = NONCE_LEN + AUTH_TAG_LEN
BLOCK_OVERHEAD = NONCE_LEN + AUTH_TAG_LEN
)
type CryptFS struct {
@ -61,8 +61,3 @@ func NewCryptFS(key []byte, useOpenssl bool) *CryptFS {
func (be *CryptFS) PlainBS() uint64 {
return be.plainBS
}
// Get ciphertext block size
func (be *CryptFS) CipherBS() uint64 {
return be.cipherBS
}

View File

@ -12,11 +12,6 @@ import (
"os"
)
const (
// A block of 4124 zero bytes has this md5
ZeroBlockMd5 = "64331af89bd15a987b39855338336237"
)
// md5sum - debug helper, return md5 hex string
func md5sum(buf []byte) string {
rawHash := md5.Sum(buf)
@ -110,106 +105,6 @@ func (be *CryptFS) EncryptBlock(plaintext []byte, blockNo uint64, fileId []byte)
return ciphertext
}
// Split a plaintext byte range into (possibly partial) blocks
func (be *CryptFS) SplitRange(offset uint64, length uint64) []intraBlock {
var b intraBlock
var parts []intraBlock
b.fs = be
for length > 0 {
b.BlockNo = offset / be.plainBS
b.Skip = offset % be.plainBS
// Minimum of remaining data and remaining space in the block
b.Length = be.minu64(length, be.plainBS-b.Skip)
parts = append(parts, b)
offset += b.Length
length -= b.Length
}
return parts
}
// PlainSize - calculate plaintext size from ciphertext size
func (be *CryptFS) PlainSize(size uint64) uint64 {
// Zero sized files stay zero-sized
if size == 0 {
return 0
}
// Account for header
size -= HEADER_LEN
overhead := be.cipherBS - be.plainBS
nBlocks := (size + be.cipherBS - 1) / be.cipherBS
if nBlocks*overhead > size {
Warn.Printf("PlainSize: Negative size, returning 0 instead\n")
return 0
}
size -= nBlocks * overhead
return size
}
// CipherSize - calculate ciphertext size from plaintext size
func (be *CryptFS) CipherSize(size uint64) uint64 {
overhead := be.cipherBS - be.plainBS
nBlocks := (size + be.plainBS - 1) / be.plainBS
size += nBlocks * overhead
return size
}
func (be *CryptFS) minu64(x uint64, y uint64) uint64 {
if x < y {
return x
}
return y
}
// CiphertextRange - Get byte range in backing ciphertext corresponding
// to plaintext range. Returns a range aligned to ciphertext blocks.
func (be *CryptFS) CiphertextRange(offset uint64, length uint64) (alignedOffset uint64, alignedLength uint64, skipBytes int) {
// Decrypting the ciphertext will yield too many plaintext bytes. Skip this number
// of bytes from the front.
skip := offset % be.plainBS
firstBlockNo := offset / be.plainBS
lastBlockNo := (offset + length - 1) / be.plainBS
alignedOffset = HEADER_LEN + firstBlockNo * be.cipherBS
alignedLength = (lastBlockNo - firstBlockNo + 1) * be.cipherBS
skipBytes = int(skip)
return alignedOffset, alignedLength, skipBytes
}
// Get the byte range in the ciphertext corresponding to blocks
// (full blocks!)
func (be *CryptFS) JoinCiphertextRange(blocks []intraBlock) (uint64, uint64) {
offset, _ := blocks[0].CiphertextRange()
last := blocks[len(blocks)-1]
length := (last.BlockNo - blocks[0].BlockNo + 1) * be.cipherBS
return offset, length
}
// Crop plaintext that correspons to complete cipher blocks down to what is
// requested according to "iblocks"
func (be *CryptFS) CropPlaintext(plaintext []byte, blocks []intraBlock) []byte {
offset := blocks[0].Skip
last := blocks[len(blocks)-1]
length := (last.BlockNo - blocks[0].BlockNo + 1) * be.plainBS
var cropped []byte
if offset+length > uint64(len(plaintext)) {
cropped = plaintext[offset:]
} else {
cropped = plaintext[offset : offset+length]
}
return cropped
}
// MergeBlocks - Merge newData into oldData at offset
// New block may be bigger than both newData and oldData
func (be *CryptFS) MergeBlocks(oldData []byte, newData []byte, offset int) []byte {
@ -230,13 +125,3 @@ func (be *CryptFS) MergeBlocks(oldData []byte, newData []byte, offset int) []byt
}
return out[0:outLen]
}
// Get the block number at plain-text offset
func (be *CryptFS) BlockNoPlainOff(plainOffset uint64) uint64 {
return plainOffset / be.plainBS
}
// Get the block number at ciphter-text offset
func (be *CryptFS) BlockNoCipherOff(cipherOffset uint64) uint64 {
return (cipherOffset - HEADER_LEN) / be.cipherBS
}

View File

@ -10,15 +10,15 @@ import (
)
const (
HEADER_CURRENT_VERSION = 1 // Current on-disk-format version
HEADER_VERSION_LEN = 2 // uint16
HEADER_ID_LEN = 16 // 128 bit random file id
HEADER_LEN = HEADER_VERSION_LEN + HEADER_ID_LEN // Total header length
HEADER_CURRENT_VERSION = 1 // Current on-disk-format version
HEADER_VERSION_LEN = 2 // uint16
HEADER_ID_LEN = 16 // 128 bit random file id
HEADER_LEN = HEADER_VERSION_LEN + HEADER_ID_LEN // Total header length
)
type FileHeader struct {
Version uint16
Id []byte
Id []byte
}
// Pack - serialize fileHeader object

View File

@ -19,13 +19,13 @@ func (ib *intraBlock) IsPartial() bool {
// CiphertextRange - get byte range in ciphertext file corresponding to BlockNo
// (complete block)
func (ib *intraBlock) CiphertextRange() (offset uint64, length uint64) {
return HEADER_LEN + ib.BlockNo * ib.fs.cipherBS, ib.fs.cipherBS
return ib.fs.BlockNoToCipherOff(ib.BlockNo), ib.fs.cipherBS
}
// PlaintextRange - get byte range in plaintext corresponding to BlockNo
// (complete block)
func (ib *intraBlock) PlaintextRange() (offset uint64, length uint64) {
return ib.BlockNo * ib.fs.plainBS, ib.fs.plainBS
return ib.fs.BlockNoToPlainOff(ib.BlockNo), ib.fs.plainBS
}
// CropBlock - crop a potentially larger plaintext block down to the relevant part
@ -37,3 +37,15 @@ func (ib *intraBlock) CropBlock(d []byte) []byte {
}
return d[ib.Skip:lenWant]
}
// Ciphertext range corresponding to the sum of all "blocks" (complete blocks)
func (ib *intraBlock) JointCiphertextRange(blocks []intraBlock) (offset uint64, length uint64) {
firstBlock := blocks[0]
lastBlock := blocks[len(blocks)-1]
offset = ib.fs.BlockNoToCipherOff(firstBlock.BlockNo)
offsetLast := ib.fs.BlockNoToCipherOff(lastBlock.BlockNo)
length = offsetLast + ib.fs.cipherBS - offset
return offset, length
}

View File

@ -1,8 +1,6 @@
package main
import (
"runtime"
"sync"
"bytes"
"crypto/md5"
"encoding/hex"
@ -11,6 +9,8 @@ import (
"io/ioutil"
"os"
"os/exec"
"runtime"
"sync"
"testing"
)
@ -121,7 +121,7 @@ func testWriteN(t *testing.T, fn string, n int) string {
}
file.Close()
verifySize(t, plainDir + fn, n)
verifySize(t, plainDir+fn, n)
bin := md5.Sum(d)
hashWant := hex.EncodeToString(bin[:])
@ -244,12 +244,12 @@ func TestFileHoles(t *testing.T) {
}
func sContains(haystack []string, needle string) bool {
for _, element := range haystack {
if element == needle {
return true
}
}
return false
for _, element := range haystack {
if element == needle {
return true
}
}
return false
}
func TestRmwRace(t *testing.T) {
@ -313,10 +313,10 @@ func TestRmwRace(t *testing.T) {
goodMd5[m] = goodMd5[m] + 1
/*
if m == "6c1660fdabccd448d1359f27b3db3c99" {
fmt.Println(hex.Dump(buf))
t.FailNow()
}
if m == "6c1660fdabccd448d1359f27b3db3c99" {
fmt.Println(hex.Dump(buf))
t.FailNow()
}
*/
}
fmt.Println(goodMd5)

View File

@ -128,8 +128,10 @@ func (f *file) doRead(off uint64, length uint64) ([]byte, fuse.Status) {
}
// Read the backing ciphertext in one go
alignedOffset, alignedLength, skip := f.cfs.CiphertextRange(off, length)
cryptfs.Debug.Printf("CiphertextRange(%d, %d) -> %d, %d, %d\n", off, length, alignedOffset, alignedLength, skip)
blocks := f.cfs.ExplodePlainRange(off, length)
alignedOffset, alignedLength := blocks[0].JointCiphertextRange(blocks)
skip := blocks[0].Skip
cryptfs.Debug.Printf("JointCiphertextRange(%d, %d) -> %d, %d, %d\n", off, length, alignedOffset, alignedLength, skip)
ciphertext := make([]byte, int(alignedLength))
f.fdLock.Lock()
n, err := f.fd.ReadAt(ciphertext, int64(alignedOffset))
@ -141,27 +143,27 @@ func (f *file) doRead(off uint64, length uint64) ([]byte, fuse.Status) {
// Truncate ciphertext buffer down to actually read bytes
ciphertext = ciphertext[0:n]
blockNo := (alignedOffset - cryptfs.HEADER_LEN) / f.cfs.CipherBS()
cryptfs.Debug.Printf("ReadAt offset=%d bytes (%d blocks), want=%d, got=%d\n", alignedOffset, blockNo, alignedLength, n)
firstBlockNo := blocks[0].BlockNo
cryptfs.Debug.Printf("ReadAt offset=%d bytes (%d blocks), want=%d, got=%d\n", alignedOffset, firstBlockNo, alignedLength, n)
// Decrypt it
plaintext, err := f.cfs.DecryptBlocks(ciphertext, blockNo, f.header.Id)
plaintext, err := f.cfs.DecryptBlocks(ciphertext, firstBlockNo, f.header.Id)
if err != nil {
blockNo := (alignedOffset + uint64(len(plaintext))) / f.cfs.PlainBS()
cipherOff := cryptfs.HEADER_LEN + blockNo * f.cfs.CipherBS()
plainOff := blockNo * f.cfs.PlainBS()
cryptfs.Warn.Printf("ino%d: doRead: corrupt block #%d (plainOff=%d/%d, cipherOff=%d/%d)\n",
f.ino, blockNo, plainOff, f.cfs.PlainBS(), cipherOff, f.cfs.CipherBS())
curruptBlockNo := firstBlockNo + f.cfs.PlainOffToBlockNo(uint64(len(plaintext)))
cipherOff := f.cfs.BlockNoToCipherOff(curruptBlockNo)
plainOff := f.cfs.BlockNoToPlainOff(curruptBlockNo)
cryptfs.Warn.Printf("ino%d: doRead: corrupt block #%d (plainOff=%d, cipherOff=%d)\n",
f.ino, curruptBlockNo, plainOff, cipherOff)
return nil, fuse.EIO
}
// Crop down to the relevant part
var out []byte
lenHave := len(plaintext)
lenWant := skip + int(length)
lenWant := int(skip + length)
if lenHave > lenWant {
out = plaintext[skip : skip+int(length)]
} else if lenHave > skip {
out = plaintext[skip:lenWant]
} else if lenHave > int(skip) {
out = plaintext[skip:lenHave]
}
// else: out stays empty, file was smaller than the requested offset
@ -216,7 +218,7 @@ func (f *file) doWrite(data []byte, off int64) (uint32, fuse.Status) {
var written uint32
status := fuse.OK
dataBuf := bytes.NewBuffer(data)
blocks := f.cfs.SplitRange(uint64(off), uint64(len(data)))
blocks := f.cfs.ExplodePlainRange(uint64(off), uint64(len(data)))
for _, b := range blocks {
blockData := dataBuf.Next(int(b.Length))
@ -239,11 +241,7 @@ func (f *file) doWrite(data []byte, off int64) (uint32, fuse.Status) {
blockOffset, _ := b.CiphertextRange()
blockData = f.cfs.EncryptBlock(blockData, b.BlockNo, f.header.Id)
cryptfs.Debug.Printf("ino%d: Writing %d bytes to block #%d, md5=%s\n",
f.ino, len(blockData) - cryptfs.BLOCK_OVERHEAD, b.BlockNo, cryptfs.Debug.Md5sum(blockData))
if len(blockData) != int(f.cfs.CipherBS()) {
cryptfs.Debug.Printf("ino%d: Writing partial block #%d (%d bytes)\n",
f.ino, b.BlockNo, len(blockData) - cryptfs.BLOCK_OVERHEAD)
}
f.ino, len(blockData)-cryptfs.BLOCK_OVERHEAD, b.BlockNo, cryptfs.Debug.Md5sum(blockData))
f.fdLock.Lock()
_, err := f.fd.WriteAt(blockData, int64(blockOffset))
f.fdLock.Unlock()
@ -267,7 +265,7 @@ func (f *file) Write(data []byte, off int64) (uint32, fuse.Status) {
cryptfs.Warn.Printf("Write: Fstat failed: %v\n", err)
return 0, fuse.ToStatus(err)
}
plainSize := f.cfs.PlainSize(uint64(fi.Size()))
plainSize := f.cfs.CipherSizeToPlainSize(uint64(fi.Size()))
if f.createsHole(plainSize, off) {
status := f.zeroPad(plainSize)
if status != fuse.OK {
@ -332,7 +330,7 @@ func (f *file) Truncate(newSize uint64) fuse.Status {
cryptfs.Warn.Printf("Truncate: Fstat failed: %v\n", err)
return fuse.ToStatus(err)
}
oldSize := f.cfs.PlainSize(uint64(fi.Size()))
oldSize := f.cfs.CipherSizeToPlainSize(uint64(fi.Size()))
{
oldB := float32(oldSize) / float32(f.cfs.PlainBS())
newB := float32(newSize) / float32(f.cfs.PlainBS())
@ -350,7 +348,7 @@ func (f *file) Truncate(newSize uint64) fuse.Status {
}
}
blocks := f.cfs.SplitRange(oldSize, newSize-oldSize)
blocks := f.cfs.ExplodePlainRange(oldSize, newSize-oldSize)
for _, b := range blocks {
// First and last block may be partial
if b.IsPartial() {
@ -374,9 +372,9 @@ func (f *file) Truncate(newSize uint64) fuse.Status {
return fuse.OK
} else {
// File shrinks
blockNo := f.cfs.BlockNoPlainOff(newSize)
cipherOff := cryptfs.HEADER_LEN + blockNo * f.cfs.CipherBS()
plainOff := blockNo * f.cfs.PlainBS()
blockNo := f.cfs.PlainOffToBlockNo(newSize)
cipherOff := f.cfs.BlockNoToCipherOff(blockNo)
plainOff := f.cfs.BlockNoToPlainOff(blockNo)
lastBlockLen := newSize - plainOff
var data []byte
if lastBlockLen > 0 {
@ -430,7 +428,7 @@ func (f *file) GetAttr(a *fuse.Attr) fuse.Status {
return fuse.ToStatus(err)
}
a.FromStat(&st)
a.Size = f.cfs.PlainSize(a.Size)
a.Size = f.cfs.CipherSizeToPlainSize(a.Size)
return fuse.OK
}

View File

@ -7,8 +7,8 @@ import (
// Will a write to offset "off" create a file hole?
func (f *file) createsHole(plainSize uint64, off int64) bool {
nextBlock := f.cfs.BlockNoPlainOff(plainSize)
targetBlock := f.cfs.BlockNoPlainOff(uint64(off))
nextBlock := f.cfs.PlainOffToBlockNo(plainSize)
targetBlock := f.cfs.PlainOffToBlockNo(uint64(off))
if targetBlock > nextBlock {
return true
}

View File

@ -41,7 +41,7 @@ func (fs *FS) GetAttr(name string, context *fuse.Context) (*fuse.Attr, fuse.Stat
return a, status
}
if a.IsRegular() {
a.Size = fs.PlainSize(a.Size)
a.Size = fs.CipherSizeToPlainSize(a.Size)
} else if a.IsSymlink() {
target, _ := fs.Readlink(name, context)
a.Size = uint64(len(target))

View File

@ -2,11 +2,10 @@
set -eux
cd cryptfs
go build
go test
cd ..
for i in ./cryptfs .
do
go build
go test
go build $i
go test $i
done