diff options
Diffstat (limited to 'cryptfs')
| -rw-r--r-- | cryptfs/file.go | 148 | ||||
| -rw-r--r-- | cryptfs/fs.go | 201 | ||||
| -rw-r--r-- | cryptfs/log.go | 19 | ||||
| -rw-r--r-- | cryptfs/nonce.go | 55 | 
4 files changed, 423 insertions, 0 deletions
diff --git a/cryptfs/file.go b/cryptfs/file.go new file mode 100644 index 0000000..77262d4 --- /dev/null +++ b/cryptfs/file.go @@ -0,0 +1,148 @@ +package cryptfs + +import ( +	"fmt" +	"os" +	"io" +	"errors" +	"crypto/cipher" +) + +type File struct { +	file *os.File +	gcm cipher.AEAD +	plainBS	int64 +	cipherBS int64 +} + +// readCipherBlock - Read ciphertext block number "blockNo", decrypt, +// return plaintext +func (be *File) readCipherBlock(blockNo int64) ([]byte, error) { +	off := blockNo * int64(be.cipherBS) +	buf := make([]byte, be.cipherBS) + +	readN, err := be.file.ReadAt(buf, off) + +	if err != nil && err != io.EOF { +		return nil, err +	} + +	// Truncate buffer to actually read bytes +	buf = buf[:readN] + +	// Empty block?file:///home/jakob/go/src/github.com/rfjakob/gocryptfs-bazil/backend/backend.go + +	if len(buf) == 0 { +		return buf, nil +	} + +	if len(buf) < NONCE_LEN { +		warn.Printf("readCipherBlock: Block is too short: %d bytes\n", len(buf)) +		return nil, errors.New("Block is too short") +	} + +	// Extract nonce +	nonce := buf[:NONCE_LEN] +	buf = buf[NONCE_LEN:] + +	// Decrypt +	var plainBuf []byte +	plainBuf, err = be.gcm.Open(plainBuf, nonce, buf, nil) +	if err != nil { +		fmt.Printf("gcm.Open() failed: %d\n", err) +		return nil, err +	} + +	return plainBuf, nil +} + +// intraBlock identifies a part of a file block +type intraBlock struct { +	blockNo int64  // Block number in file +	offset  int64  // Offset into block plaintext +	length  int64  // Length of data from this block +} + +// Split a plaintext byte range into (possible partial) blocks +func (be *File) splitRange(offset int64, length int64) []intraBlock { +	var b intraBlock +	var parts []intraBlock + +	for length > 0 { +		b.blockNo = offset / be.plainBS +		b.offset = offset % be.plainBS +		b.length = be.min64(length, be.plainBS - b.offset) +		parts = append(parts, b) +		offset += b.length +		length -= b.length +	} +	return parts +} + +func (be *File) min64(x int64, y int64) int64 { +	if x < y { +		return x +	} +	return y +} + +// writeCipherBlock - Encrypt plaintext and write it to file block "blockNo" +func (be *File) writeCipherBlock(blockNo int64, plain []byte) error { + +	if int64(len(plain)) > be.plainBS { +		panic("writeCipherBlock: Cannot write block that is larger than plainBS") +	} + +	// Get fresh nonce +	nonce := gcmNonce.Get() +	// Encrypt data and append to nonce +	cipherBuf := be.gcm.Seal(nonce, nonce, plain, nil) + +	// WriteAt retries short writes autmatically +	written, err := be.file.WriteAt(cipherBuf, blockNo * be.cipherBS) + +	debug.Printf("writeCipherBlock: wrote %d ciphertext bytes to block %d\n", +		written, blockNo) + +	return err +} + +// Perform RMW cycle on block +// Write "data" into file location specified in "b" +func (be *File) rmwWrite(b intraBlock, data []byte, f *os.File) error { +	if b.length != int64(len(data)) { +		panic("Length mismatch") +	} + +	oldBlock, err := be.readCipherBlock(b.blockNo) +	if err != nil { +		return err +	} +	newBlockLen := b.offset + b.length +	debug.Printf("newBlockLen := %d + %d\n", b.offset, b.length) +	var newBlock []byte + +	// Write goes beyond the old block and grows the file? +	// Must create a bigger newBlock +	if newBlockLen > int64(len(oldBlock)) { +		newBlock = make([]byte, newBlockLen) +	} else { +		newBlock = make([]byte, len(oldBlock)) +	} + +	// Fill with old data +	copy(newBlock, oldBlock) +	// Then overwrite the relevant parts with new data +	copy(newBlock[b.offset:b.offset + b.length], data) + +	// Actual write +	err = be.writeCipherBlock(b.blockNo, newBlock) + +	if err != nil { +		// An incomplete write to a ciphertext block means that the whole block +		// is destroyed. +		fmt.Printf("rmwWrite: Write error: %s\n", err) +	} + +	return err +} diff --git a/cryptfs/fs.go b/cryptfs/fs.go new file mode 100644 index 0000000..b40dcf3 --- /dev/null +++ b/cryptfs/fs.go @@ -0,0 +1,201 @@ +package cryptfs + +import ( +	"crypto/cipher" +	"crypto/aes" +	"fmt" +	"strings" +	"encoding/base64" +	"errors" +	"os" +) + +const ( +	NONCE_LEN = 12 +	AUTH_TAG_LEN = 16 +	DEFAULT_PLAINBS = 4096 + +	ENCRYPT = true +	DECRYPT = false +) + +type FS struct { +	blockCipher cipher.Block +	gcm cipher.AEAD +	plainBS	int64 +	cipherBS int64 +} + +func New(key [16]byte) *FS { + +	b, err := aes.NewCipher(key[:]) +	if err != nil { +		panic(err) +	} + +	g, err := cipher.NewGCM(b) +	if err != nil { +		panic(err) +	} + +	return &FS{ +		blockCipher: b, +		gcm: g, +		plainBS: DEFAULT_PLAINBS, +		cipherBS: DEFAULT_PLAINBS + NONCE_LEN + AUTH_TAG_LEN, +	} +} + +func (fs *FS) NewFile(f *os.File) *File { +	return &File { +		file: f, +		gcm: fs.gcm, +		plainBS: fs.plainBS, +		cipherBS: fs.cipherBS, +	} +} + +// DecryptName - decrypt filename +func (be *FS) decryptName(cipherName string) (string, error) { + +	bin, err := base64.URLEncoding.DecodeString(cipherName) +	if err != nil { +		return "", err +	} + +	if len(bin) % aes.BlockSize != 0 { +		return "", errors.New(fmt.Sprintf("Name len=%d is not a multiple of 16", len(bin))) +	} + +	iv := make([]byte, aes.BlockSize) // TODO ? +	cbc := cipher.NewCBCDecrypter(be.blockCipher, iv) +	cbc.CryptBlocks(bin, bin) + +	bin, err = be.unPad16(bin) +	if err != nil { +		return "", err +	} + +	plain := string(bin) +	return plain, err +} + +// EncryptName - encrypt filename +func (be *FS) encryptName(plainName string) string { + +	bin := []byte(plainName) +	bin = be.pad16(bin) + +	iv := make([]byte, 16) // TODO ? +	cbc := cipher.NewCBCEncrypter(be.blockCipher, iv) +	cbc.CryptBlocks(bin, bin) + +	cipherName64 := base64.URLEncoding.EncodeToString(bin) + +	return cipherName64 +} + +// TranslatePath - encrypt or decrypt path. Just splits the string on "/" +// and hands the parts to EncryptName() / DecryptName() +func (be *FS) translatePath(path string, op bool) (string, error) { +	var err error + +	// Empty string means root directory +	if path == "" { +		return path, err +	} + +	// Run operation on each path component +	var translatedParts []string +	parts := strings.Split(path, "/") +	for _, part := range parts { +		var newPart string +		if op == ENCRYPT { +			newPart = be.encryptName(part) +		} else { +			newPart, err = be.decryptName(part) +			if err != nil { +				return "", err +			} +		} +		translatedParts = append(translatedParts, newPart) +	} + +	return strings.Join(translatedParts, "/"), err +} + +// EncryptPath - encrypt filename or path. Just hands it to TranslatePath(). +func (be *FS) EncryptPath(path string) string { +	newPath, _ := be.translatePath(path, ENCRYPT) +	return newPath +} + +// DecryptPath - decrypt filename or path. Just hands it to TranslatePath(). +func (be *FS) DecryptPath(path string) (string, error) { +	return be.translatePath(path, DECRYPT) +} + +// plainSize - calculate plaintext size from ciphertext size +func (be *FS) PlainSize(s int64) int64 { +	// Zero sized files stay zero-sized +	if s > 0 { +		// Number of blocks +		n := s / be.cipherBS + 1 +		overhead := be.cipherBS - be.plainBS +		s -= n * overhead +	} +	return s +} + +// pad16 - pad filename to 16 byte blocks using standard PKCS#7 padding +// https://tools.ietf.org/html/rfc5652#section-6.3 +func (be *FS) pad16(orig []byte) (padded []byte) { +	oldLen := len(orig) +	if oldLen == 0 { +		panic("Padding zero-length string makes no sense") +	} +	padLen := aes.BlockSize - oldLen % aes.BlockSize +	if padLen == 0 { +		padLen = aes.BlockSize +	} +	newLen := oldLen + padLen +	padded = make([]byte, newLen) +	copy(padded, orig) +	padByte := byte(padLen) +	for i := oldLen; i < newLen; i++ { +		padded[i] = padByte +	} +	return padded +} + +// unPad16 - remove padding +func (be *FS) unPad16(orig []byte) ([]byte, error) { +	oldLen := len(orig) +	if oldLen % aes.BlockSize != 0 { +		return nil, errors.New("Unaligned size") +	} +	// The last byte is always a padding byte +	padByte := orig[oldLen -1] +	// The padding byte's value is the padding length +	padLen := int(padByte) +	// Padding must be at least 1 byte +	if padLen <= 0 { +		return nil, errors.New("Padding cannot be zero-length") +	} +	// Larger paddings make no sense +	if padLen > aes.BlockSize { +		return nil, errors.New("Padding cannot be larger than 16") +	} +	// All padding bytes must be identical +	for i := oldLen - padLen; i < oldLen; i++ { +		if orig[i] != padByte { +			return nil, errors.New(fmt.Sprintf("Padding byte at i=%d is invalid", i)) +		} +	} +	newLen := oldLen - padLen +	// Padding an empty string makes no sense +	if newLen == 0 { +		return nil, errors.New("Unpadded length is zero") +	} +	return orig[0:newLen], nil +} diff --git a/cryptfs/log.go b/cryptfs/log.go new file mode 100644 index 0000000..947b1c3 --- /dev/null +++ b/cryptfs/log.go @@ -0,0 +1,19 @@ +package cryptfs + +import ( +	"fmt" +) + +type logChannel struct { +	enabled bool +} + +func (l logChannel) Printf(format string, args ...interface{}) { +	if l.enabled == true { +		fmt.Printf(format, args...) +	} +} + + +var debug = logChannel{true} +var warn = logChannel{true} diff --git a/cryptfs/nonce.go b/cryptfs/nonce.go new file mode 100644 index 0000000..3803b8c --- /dev/null +++ b/cryptfs/nonce.go @@ -0,0 +1,55 @@ +package cryptfs + +import ( +	"encoding/binary" +	"encoding/hex" +	"sync" +	"crypto/rand" +) + +type nonce96 struct { +	lock sync.Mutex +	high32 uint32 +	low64 uint64 +	ready int +} + +var gcmNonce nonce96 + +func (n *nonce96) randBytes(len int) []byte { +	b := make([]byte, len) +	_, err := rand.Read(b) +	if err != nil { +		panic("Could not get random bytes for nonce") +	} +	return b +} + +func (n *nonce96) init() { +	b := n.randBytes(8) +	n.low64 = binary.BigEndian.Uint64(b) +	b = n.randBytes(4) +	n.high32 = binary.BigEndian.Uint32(b) +	n.ready = 1 +	return +} + +func (n *nonce96) Get() []byte { +	n.lock.Lock() +	if n.ready == 0 { +		n.init() +	} +	n.low64++ +	if n.low64 == 0 { +		// Counter has wrapped +		n.high32++ +	} +	r := make([]byte, 12) +	binary.BigEndian.PutUint32(r[0:4], n.high32) +	binary.BigEndian.PutUint64(r[4:12], n.low64) +	n.lock.Unlock() + +	debug.Printf("nonce96.Get(): %s\n", hex.EncodeToString(r)) + +	return r +}  | 
