aboutsummaryrefslogtreecommitdiff
path: root/cryptfs
diff options
context:
space:
mode:
authorJakob Unterwurzacher2015-11-01 01:32:33 +0100
committerJakob Unterwurzacher2015-11-01 01:38:27 +0100
commit76311b60f2e208dbd93e1e7b6e9794770c14fede (patch)
treefa71d48744fd1ad6de26aaadd03f2440e4d4103d /cryptfs
parent73fa8efdb27172210b9751eb86689287db0b1170 (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.
Diffstat (limited to 'cryptfs')
-rw-r--r--cryptfs/config_file.go14
-rw-r--r--cryptfs/content_test.go21
-rw-r--r--cryptfs/cryptfs.go1
-rw-r--r--cryptfs/cryptfs_content.go20
-rw-r--r--cryptfs/file_header.go56
-rw-r--r--cryptfs/intrablock.go2
6 files changed, 96 insertions, 18 deletions
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