diff options
Diffstat (limited to 'internal')
31 files changed, 1713 insertions, 0 deletions
| diff --git a/internal/configfile/config_file.go b/internal/configfile/config_file.go new file mode 100644 index 0000000..0128acc --- /dev/null +++ b/internal/configfile/config_file.go @@ -0,0 +1,195 @@ +package configfile + +import ( +	"encoding/json" +	"fmt" +	"io/ioutil" +	"log" + +	"github.com/rfjakob/gocryptfs/internal/cryptocore" +	"github.com/rfjakob/gocryptfs/internal/contentenc" +	"github.com/rfjakob/gocryptfs/internal/toggledlog" +) +import "os" + +const ( +	// The dot "." is not used in base64url (RFC4648), hence +	// we can never clash with an encrypted file. +	ConfDefaultName = "gocryptfs.conf" +) + +type ConfFile struct { +	// File the config is saved to. Not exported to JSON. +	filename string +	// Encrypted AES key, unlocked using a password hashed with scrypt +	EncryptedKey []byte +	// Stores parameters for scrypt hashing (key derivation) +	ScryptObject scryptKdf +	// The On-Disk-Format version this filesystem uses +	Version uint16 +	// List of feature flags this filesystem has enabled. +	// If gocryptfs encounters a feature flag it does not support, it will refuse +	// mounting. This mechanism is analogous to the ext4 feature flags that are +	// stored in the superblock. +	FeatureFlags []string +} + +// CreateConfFile - create a new config with a random key encrypted with +// "password" and write it to "filename". +// Uses scrypt with cost parameter logN. +func CreateConfFile(filename string, password string, plaintextNames bool, logN int) error { +	var cf ConfFile +	cf.filename = filename +	cf.Version = contentenc.CurrentVersion + +	// Generate new random master key +	key := cryptocore.RandBytes(cryptocore.KeyLen) + +	// Encrypt it using the password +	// This sets ScryptObject and EncryptedKey +	cf.EncryptKey(key, password, logN) + +	// Set feature flags +	cf.FeatureFlags = append(cf.FeatureFlags, FlagGCMIV128) +	if plaintextNames { +		cf.FeatureFlags = append(cf.FeatureFlags, FlagPlaintextNames) +	} else { +		cf.FeatureFlags = append(cf.FeatureFlags, FlagDirIV) +		cf.FeatureFlags = append(cf.FeatureFlags, FlagEMENames) +	} + +	// Write file to disk +	return cf.WriteFile() +} + +// LoadConfFile - read config file from disk and decrypt the +// contained key using password. +// +// Returns the decrypted key and the ConfFile object +func LoadConfFile(filename string, password string) ([]byte, *ConfFile, error) { +	var cf ConfFile +	cf.filename = filename + +	// Read from disk +	js, err := ioutil.ReadFile(filename) +	if err != nil { +		return nil, nil, err +	} + +	// Unmarshal +	err = json.Unmarshal(js, &cf) +	if err != nil { +		toggledlog.Warn.Printf("Failed to unmarshal config file") +		return nil, nil, err +	} + +	if cf.Version != contentenc.CurrentVersion { +		return nil, nil, fmt.Errorf("Unsupported on-disk format %d", cf.Version) +	} + +	for _, flag := range cf.FeatureFlags { +		if cf.isFeatureFlagKnown(flag) == false { +			return nil, nil, fmt.Errorf("Unsupported feature flag %s", flag) +		} +	} + +	// Generate derived key from password +	scryptHash := cf.ScryptObject.DeriveKey(password) + +	// Unlock master key using password-based key +	// We use stock go GCM instead of OpenSSL here as speed is not important +	// and we get better error messages +	cc := cryptocore.New(scryptHash, false, false) +	ce := contentenc.New(cc, 4096) + +	key, err := ce.DecryptBlock(cf.EncryptedKey, 0, nil) +	if err != nil { +		toggledlog.Warn.Printf("failed to unlock master key: %s", err.Error()) +		toggledlog.Warn.Printf("Password incorrect.") +		return nil, nil, err +	} + +	return key, &cf, nil +} + +// EncryptKey - encrypt "key" using an scrypt hash generated from "password" +// and store it in cf.EncryptedKey. +// Uses scrypt with cost parameter logN and stores the scrypt parameters in +// cf.ScryptObject. +func (cf *ConfFile) EncryptKey(key []byte, password string, logN int) { +	// Generate derived key from password +	cf.ScryptObject = NewScryptKdf(logN) +	scryptHash := cf.ScryptObject.DeriveKey(password) + +	// Lock master key using password-based key +	cc := cryptocore.New(scryptHash, false, false) +	ce := contentenc.New(cc, 4096) +	cf.EncryptedKey = ce.EncryptBlock(key, 0, nil) +} + +// WriteFile - write out config in JSON format to file "filename.tmp" +// then rename over "filename". +// This way a password change atomically replaces the file. +func (cf *ConfFile) WriteFile() error { +	tmp := cf.filename + ".tmp" +	// 0400 permissions: gocryptfs.conf should be kept secret and never be written to. +	fd, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0400) +	if err != nil { +		return err +	} +	js, err := json.MarshalIndent(cf, "", "\t") +	if err != nil { +		return err +	} +	_, err = fd.Write(js) +	if err != nil { +		return err +	} +	err = fd.Sync() +	if err != nil { +		return err +	} +	err = fd.Close() +	if err != nil { +		return err +	} +	err = os.Rename(tmp, cf.filename) +	if err != nil { +		return err +	} + +	return nil +} + +const ( +	// Understood Feature Flags. +	// Also teach isFeatureFlagKnown() about any additions and +	// add it to CreateConfFile() if you want to have it enabled by default. +	FlagPlaintextNames = "PlaintextNames" +	FlagDirIV          = "DirIV" +	FlagEMENames       = "EMENames" +	FlagGCMIV128       = "GCMIV128" +) + +// Verify that we understand a feature flag +func (cf *ConfFile) isFeatureFlagKnown(flag string) bool { +	switch flag { +	case FlagPlaintextNames, FlagDirIV, FlagEMENames, FlagGCMIV128: +		return true +	default: +		return false +	} +} + +// isFeatureFlagSet - is the feature flag "flagWant" enabled? +func (cf *ConfFile) IsFeatureFlagSet(flagWant string) bool { +	if !cf.isFeatureFlagKnown(flagWant) { +		log.Panicf("BUG: Tried to use unsupported feature flag %s", flagWant) +	} +	for _, flag := range cf.FeatureFlags { +		if flag == flagWant { +			return true +		} +	} +	return false +} diff --git a/internal/configfile/config_test.go b/internal/configfile/config_test.go new file mode 100644 index 0000000..6606d22 --- /dev/null +++ b/internal/configfile/config_test.go @@ -0,0 +1,83 @@ +package configfile + +import ( +	"fmt" +	"testing" +	"time" +) + +func TestLoadV1(t *testing.T) { +	_, _, err := LoadConfFile("config_test/v1.conf", "test") +	if err == nil { +		t.Errorf("Outdated v1 config file must fail to load but it didn't") +	} else if testing.Verbose() { +		fmt.Print(err) +	} +} + +// Load a known-good config file and verify that it takes at least 100ms +// (brute-force protection) +func TestLoadV2(t *testing.T) { +	t1 := time.Now() + +	_, _, err := LoadConfFile("config_test/v2.conf", "foo") +	if err != nil { +		t.Errorf("Could not load v2 config file: %v", err) +	} + +	elapsed := time.Since(t1) +	if elapsed < 100*time.Millisecond { +		t.Errorf("scrypt calculation runs too fast: %d ms", elapsed/time.Millisecond) +	} +} + +func TestLoadV2PwdError(t *testing.T) { +	if !testing.Verbose() { +		Warn.Enabled = false +	} +	_, _, err := LoadConfFile("config_test/v2.conf", "wrongpassword") +	if err == nil { +		t.Errorf("Loading with wrong password must fail but it didn't") +	} +} + +func TestLoadV2Feature(t *testing.T) { +	_, _, err := LoadConfFile("config_test/PlaintextNames.conf", "test") +	if err != nil { +		t.Errorf("Could not load v2 PlaintextNames config file: %v", err) +	} +} + +func TestLoadV2StrangeFeature(t *testing.T) { +	_, _, err := LoadConfFile("config_test/StrangeFeature.conf", "test") +	if err == nil { +		t.Errorf("Loading unknown feature must fail but it didn't") +	} else if testing.Verbose() { +		fmt.Print(err) +	} +} + +func TestCreateConfFile(t *testing.T) { +	err := CreateConfFile("config_test/tmp.conf", "test", false, 10) +	if err != nil { +		t.Fatal(err) +	} +	_, _, err = LoadConfFile("config_test/tmp.conf", "test") +	if err != nil { +		t.Fatal(err) +	} + +} + +func TestIsFeatureFlagKnown(t *testing.T) { +	var cf ConfFile +	if !cf.isFeatureFlagKnown(FlagDirIV) { +		t.Errorf("This flag should be known") +	} +	if !cf.isFeatureFlagKnown(FlagPlaintextNames) { +		t.Errorf("This flag should be known") +	} +	if cf.isFeatureFlagKnown("StrangeFeatureFlag") { +		t.Errorf("This flag should be NOT known") +	} +} diff --git a/internal/configfile/config_test/.gitignore b/internal/configfile/config_test/.gitignore new file mode 100644 index 0000000..0720169 --- /dev/null +++ b/internal/configfile/config_test/.gitignore @@ -0,0 +1 @@ +tmp.conf diff --git a/internal/configfile/config_test/PlaintextNames.conf b/internal/configfile/config_test/PlaintextNames.conf new file mode 100644 index 0000000..c1ff8cc --- /dev/null +++ b/internal/configfile/config_test/PlaintextNames.conf @@ -0,0 +1,14 @@ +{ +	"EncryptedKey": "rG4u0argMq02V5G9Fa+gAaaHtNrj3wn7OZjP44hWOzO4yBFtn+Qn3PW4V6LMuKmGLEhyktCyWOI3K8lj", +	"ScryptObject": { +		"Salt": "bRjq1V63u5ML3FoTWx/GBXUhUVpTunOX3DPxS+yPjg0=", +		"N": 65536, +		"R": 8, +		"P": 1, +		"KeyLen": 32 +	}, +	"Version": 2, +	"FeatureFlags": [ +		"PlaintextNames" +	] +} diff --git a/internal/configfile/config_test/StrangeFeature.conf b/internal/configfile/config_test/StrangeFeature.conf new file mode 100644 index 0000000..6a97781 --- /dev/null +++ b/internal/configfile/config_test/StrangeFeature.conf @@ -0,0 +1,14 @@ +{ +	"EncryptedKey": "rG4u0argMq02V5G9Fa+gAaaHtNrj3wn7OZjP44hWOzO4yBFtn+Qn3PW4V6LMuKmGLEhyktCyWOI3K8lj", +	"ScryptObject": { +		"Salt": "bRjq1V63u5ML3FoTWx/GBXUhUVpTunOX3DPxS+yPjg0=", +		"N": 65536, +		"R": 8, +		"P": 1, +		"KeyLen": 32 +	}, +	"Version": 2, +	"FeatureFlags": [ +		"StrangeFeatureFlag" +	] +} diff --git a/internal/configfile/config_test/v1.conf b/internal/configfile/config_test/v1.conf new file mode 100644 index 0000000..588a25a --- /dev/null +++ b/internal/configfile/config_test/v1.conf @@ -0,0 +1,11 @@ +{ +	"EncryptedKey": "t6YAvFQJvbv46c93bHQ5IZnvNz80DA9cohGoSPL/2M257LuIigow6jbr8b9HhnbDqHTCcz7aKkMDzneF", +	"ScryptObject": { +		"Salt": "yT4yQmmRmVNx2P0tJrUswk5SQzZaL6Z8kUteAoNJkXM=", +		"N": 65536, +		"R": 8, +		"P": 1, +		"KeyLen": 32 +	}, +	"Version": 1 +} diff --git a/internal/configfile/config_test/v2.conf b/internal/configfile/config_test/v2.conf new file mode 100644 index 0000000..8ef3dcf --- /dev/null +++ b/internal/configfile/config_test/v2.conf @@ -0,0 +1,11 @@ +{ +	"EncryptedKey": "RvxJnZWKTBSU21+7xbl08xlZyNyUCkpIqlK8Z51TUrRiBhqqNPxbdk1WXMvmOf/YzZ85Xbyz+DGM+SDf", +	"ScryptObject": { +		"Salt": "2OrFRfdW/5SanbMXM3TMINmfMO6oYU9awG+NZ77V8E8=", +		"N": 65536, +		"R": 8, +		"P": 1, +		"KeyLen": 32 +	}, +	"Version": 2 +} diff --git a/internal/configfile/kdf.go b/internal/configfile/kdf.go new file mode 100644 index 0000000..f1a7a40 --- /dev/null +++ b/internal/configfile/kdf.go @@ -0,0 +1,57 @@ +package configfile + +import ( +	"fmt" +	"math" +	"os" + +	"golang.org/x/crypto/scrypt" + +	"github.com/rfjakob/gocryptfs/internal/cryptocore" +) + +const ( +	// 1 << 16 uses 64MB of memory, +	// takes 4 seconds on my Atom Z3735F netbook +	ScryptDefaultLogN = 16 +) + +type scryptKdf struct { +	Salt   []byte +	N      int +	R      int +	P      int +	KeyLen int +} + +func NewScryptKdf(logN int) scryptKdf { +	var s scryptKdf +	s.Salt = cryptocore.RandBytes(cryptocore.KeyLen) +	if logN <= 0 { +		s.N = 1 << ScryptDefaultLogN +	} else { +		if logN < 10 { +			fmt.Println("Error: scryptn below 10 is too low to make sense. Aborting.") +			os.Exit(1) +		} +		s.N = 1 << uint32(logN) +	} +	s.R = 8 // Always 8 +	s.P = 1 // Always 1 +	s.KeyLen = cryptocore.KeyLen +	return s +} + +func (s *scryptKdf) DeriveKey(pw string) []byte { +	k, err := scrypt.Key([]byte(pw), s.Salt, s.N, s.R, s.P, s.KeyLen) +	if err != nil { +		panic(fmt.Sprintf("DeriveKey failed: %s", err.Error())) +	} +	return k +} + +// LogN - N is saved as 2^LogN, but LogN is much easier to work with. +// This function gives you LogN = Log2(N). +func (s *scryptKdf) LogN() int { +	return int(math.Log2(float64(s.N)) + 0.5) +} diff --git a/internal/configfile/kdf_test.go b/internal/configfile/kdf_test.go new file mode 100644 index 0000000..bc095ab --- /dev/null +++ b/internal/configfile/kdf_test.go @@ -0,0 +1,60 @@ +package configfile + +import ( +	"testing" +) + +/* +Results on a 2.7GHz Pentium G630: + +gocryptfs/cryptfs$ go test -bench=. +PASS +BenchmarkScrypt10-2	     300	   6021435 ns/op ... 6ms +BenchmarkScrypt11-2	     100	  11861460 ns/op +BenchmarkScrypt12-2	     100	  23420822 ns/op +BenchmarkScrypt13-2	      30	  47666518 ns/op +BenchmarkScrypt14-2	      20	  92561590 ns/op ... 92ms +BenchmarkScrypt15-2	      10	 183971593 ns/op +BenchmarkScrypt16-2	       3	 368506365 ns/op +BenchmarkScrypt17-2	       2	 755502608 ns/op ... 755ms +ok  	github.com/rfjakob/gocryptfs/cryptfs	18.772s +*/ + +func benchmarkScryptN(n int, b *testing.B) { +	kdf := NewScryptKdf(n) +	for i := 0; i < b.N; i++ { +		kdf.DeriveKey("test") +	} +} + +func BenchmarkScrypt10(b *testing.B) { +	benchmarkScryptN(10, b) +} + +func BenchmarkScrypt11(b *testing.B) { +	benchmarkScryptN(11, b) +} + +func BenchmarkScrypt12(b *testing.B) { +	benchmarkScryptN(12, b) +} + +func BenchmarkScrypt13(b *testing.B) { +	benchmarkScryptN(13, b) +} + +func BenchmarkScrypt14(b *testing.B) { +	benchmarkScryptN(14, b) +} + +func BenchmarkScrypt15(b *testing.B) { +	benchmarkScryptN(15, b) +} + +func BenchmarkScrypt16(b *testing.B) { +	benchmarkScryptN(16, b) +} + +func BenchmarkScrypt17(b *testing.B) { +	benchmarkScryptN(17, b) +} 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 +} diff --git a/internal/cryptocore/crypto_api.go b/internal/cryptocore/crypto_api.go new file mode 100644 index 0000000..c6b6869 --- /dev/null +++ b/internal/cryptocore/crypto_api.go @@ -0,0 +1,56 @@ +package cryptocore + +import ( +	"crypto/cipher" +	"crypto/aes" +	"fmt" +) + +const ( +	KeyLen          = 32 // AES-256 +	AuthTagLen      = 16 +) + +type CryptoCore struct { +	BlockCipher cipher.Block +	Gcm         cipher.AEAD +	GcmIVGen	*nonceGenerator +	IVLen		int +} + +func New(key []byte, useOpenssl bool, GCMIV128 bool) *CryptoCore { + +	if len(key) != KeyLen { +		panic(fmt.Sprintf("Unsupported key length %d", len(key))) +	} + +	// We want the IV size in bytes +	IVLen := 96 / 8 +	if GCMIV128 { +		IVLen = 128 / 8 +	} + +	// We always use built-in Go crypto for blockCipher because it is not +	// performance-critical. +	blockCipher, err := aes.NewCipher(key) +	if err != nil { +		panic(err) +	} + +	var gcm cipher.AEAD +	if useOpenssl { +		gcm = opensslGCM{key} +	} else { +		gcm, err = goGCMWrapper(blockCipher, IVLen) +		if err != nil { +			panic(err) +		} +	} + +	return &CryptoCore{ +		BlockCipher: blockCipher, +		Gcm: gcm, +		GcmIVGen:  &nonceGenerator{nonceLen: IVLen}, +		IVLen: IVLen, +	} +} diff --git a/internal/cryptocore/gcm_go1.4.go b/internal/cryptocore/gcm_go1.4.go new file mode 100644 index 0000000..dba222c --- /dev/null +++ b/internal/cryptocore/gcm_go1.4.go @@ -0,0 +1,22 @@ +// +build !go1.5 +// = go 1.4 or lower + +package cryptocore + +import ( +	"crypto/cipher" +	"fmt" +) + +// goGCMWrapper - This wrapper makes sure gocryptfs can be compiled on Go +// versions 1.4 and lower that lack NewGCMWithNonceSize(). +// 128 bit GCM IVs will not work when using built-in Go crypto, obviously, when +// compiled on 1.4. +func goGCMWrapper(bc cipher.Block, nonceSize int) (cipher.AEAD, error) { +	if nonceSize != 12 { +		Warn.Printf("128 bit GCM IVs are not supported by Go 1.4 and lower.") +		Warn.Printf("Please use openssl crypto or recompile using a newer Go runtime.") +		return nil, fmt.Errorf("128 bit GCM IVs are not supported by Go 1.4 and lower") +	} +	return cipher.NewGCM(bc) +} diff --git a/internal/cryptocore/gcm_go1.5.go b/internal/cryptocore/gcm_go1.5.go new file mode 100644 index 0000000..0c9b1a5 --- /dev/null +++ b/internal/cryptocore/gcm_go1.5.go @@ -0,0 +1,16 @@ +// +build go1.5 +// = go 1.5 or higher + +package cryptocore + +import ( +	"crypto/cipher" +) + +// goGCMWrapper - This wrapper makes sure gocryptfs can be compiled on Go +// versions 1.4 and lower that lack NewGCMWithNonceSize(). +// 128 bit GCM IVs will not work when using built-in Go crypto, obviously, when +// compiled on 1.4. +func goGCMWrapper(bc cipher.Block, nonceSize int) (cipher.AEAD, error) { +	return cipher.NewGCMWithNonceSize(bc, nonceSize) +} diff --git a/internal/cryptocore/nonce.go b/internal/cryptocore/nonce.go new file mode 100644 index 0000000..72d8588 --- /dev/null +++ b/internal/cryptocore/nonce.go @@ -0,0 +1,44 @@ +package cryptocore + +import ( +	"bytes" +	"crypto/rand" +	"encoding/binary" +	"encoding/hex" +	"fmt" + +	"github.com/rfjakob/gocryptfs/internal/toggledlog" +) + +// Get "n" random bytes from /dev/urandom or panic +func RandBytes(n int) []byte { +	b := make([]byte, n) +	_, err := rand.Read(b) +	if err != nil { +		panic("Failed to read random bytes: " + err.Error()) +	} +	return b +} + +// Return a secure random uint64 +func RandUint64() uint64 { +	b := RandBytes(8) +	return binary.BigEndian.Uint64(b) +} + +type nonceGenerator struct { +	lastNonce []byte +	nonceLen  int // bytes +} + +// Get a random "nonceLen"-byte nonce +func (n *nonceGenerator) Get() []byte { +	nonce := RandBytes(n.nonceLen) +	toggledlog.Debug.Printf("nonceGenerator.Get(): %s\n", hex.EncodeToString(nonce)) +	if bytes.Equal(nonce, n.lastNonce) { +		m := fmt.Sprintf("Got the same nonce twice: %s. This should never happen!", hex.EncodeToString(nonce)) +		panic(m) +	} +	n.lastNonce = nonce +	return nonce +} diff --git a/internal/cryptocore/openssl_aead.go b/internal/cryptocore/openssl_aead.go new file mode 100644 index 0000000..d4ed64b --- /dev/null +++ b/internal/cryptocore/openssl_aead.go @@ -0,0 +1,100 @@ +package cryptocore + +// Implements cipher.AEAD with OpenSSL backend + +import ( +	"bytes" +	"github.com/spacemonkeygo/openssl" +) + +// Supports all nonce sizes +type opensslGCM struct { +	key []byte +} + +func (be opensslGCM) Overhead() int { +	return AuthTagLen +} + +func (be opensslGCM) NonceSize() int { +	// We support any nonce size +	return -1 +} + +// Seal encrypts and authenticates plaintext, authenticates the +// additional data and appends the result to dst, returning the updated +// slice. opensslGCM supports any nonce size. +func (be opensslGCM) Seal(dst, nonce, plaintext, data []byte) []byte { + +	// Preallocate output buffer +	var cipherBuf bytes.Buffer +	cipherBuf.Grow(len(dst) + len(plaintext) + AuthTagLen) +	// Output will be appended to dst +	cipherBuf.Write(dst) + +	ectx, err := openssl.NewGCMEncryptionCipherCtx(KeyLen*8, nil, be.key, nonce) +	if err != nil { +		panic(err) +	} +	err = ectx.ExtraData(data) +	if err != nil { +		panic(err) +	} +	part, err := ectx.EncryptUpdate(plaintext) +	if err != nil { +		panic(err) +	} +	cipherBuf.Write(part) +	part, err = ectx.EncryptFinal() +	if err != nil { +		panic(err) +	} +	cipherBuf.Write(part) +	part, err = ectx.GetTag() +	if err != nil { +		panic(err) +	} +	cipherBuf.Write(part) + +	return cipherBuf.Bytes() +} + +// Open decrypts and authenticates ciphertext, authenticates the +// additional data and, if successful, appends the resulting plaintext +// to dst, returning the updated slice. The nonce must be NonceSize() +// bytes long and both it and the additional data must match the +// value passed to Seal. +// +// The ciphertext and dst may alias exactly or not at all. +func (be opensslGCM) Open(dst, nonce, ciphertext, data []byte) ([]byte, error) { + +	l := len(ciphertext) +	tag := ciphertext[l-AuthTagLen : l] +	ciphertext = ciphertext[0 : l-AuthTagLen] +	plainBuf := bytes.NewBuffer(dst) + +	dctx, err := openssl.NewGCMDecryptionCipherCtx(KeyLen*8, nil, be.key, nonce) +	if err != nil { +		return nil, err +	} +	err = dctx.ExtraData(data) +	if err != nil { +		return nil, err +	} +	part, err := dctx.DecryptUpdate(ciphertext) +	if err != nil { +		return nil, err +	} +	plainBuf.Write(part) +	err = dctx.SetTag(tag) +	if err != nil { +		return nil, err +	} +	part, err = dctx.DecryptFinal() +	if err != nil { +		return nil, err +	} +	plainBuf.Write(part) + +	return plainBuf.Bytes(), nil +} diff --git a/internal/cryptocore/openssl_benchmark.bash b/internal/cryptocore/openssl_benchmark.bash new file mode 100755 index 0000000..df29628 --- /dev/null +++ b/internal/cryptocore/openssl_benchmark.bash @@ -0,0 +1,3 @@ +#!/bin/bash + +go test -run NONE -bench BenchmarkEnc diff --git a/internal/cryptocore/openssl_test.go b/internal/cryptocore/openssl_test.go new file mode 100644 index 0000000..94b696a --- /dev/null +++ b/internal/cryptocore/openssl_test.go @@ -0,0 +1,75 @@ +package cryptocore + +// Benchmark go built-int GCM against spacemonkey openssl bindings +// +// Note: The benchmarks in this file supersede the ones in the openssl_benchmark +//       directory as they use the same code paths that gocryptfs actually uses. +// +// Run benchmark: +// go test -bench Enc + +import ( +	"crypto/aes" +	"testing" +) + +func benchmarkGoEnc(b *testing.B, plaintext []byte, key []byte, nonce []byte) (ciphertext []byte) { +	b.SetBytes(int64(len(plaintext))) +	aes, err := aes.NewCipher(key[:]) +	if err != nil { +		b.Fatal(err) +	} +	aesgcm, err := goGCMWrapper(aes, len(nonce)) +	if err != nil { +		b.Fatal(err) +	} +	// This would be fileID + blockNo +	aData := make([]byte, 24) +	b.ResetTimer() +	for i := 0; i < b.N; i++ { +		// Encrypt plaintext and append to nonce +		ciphertext = aesgcm.Seal(nonce, nonce, plaintext, aData) +	} +	return ciphertext +} + +func benchmarkOpensslEnc(b *testing.B, plaintext []byte, key []byte, nonce []byte) (ciphertext []byte) { +	b.SetBytes(int64(len(plaintext))) +	var aesgcm opensslGCM +	aesgcm.key = key +	// This would be fileID + blockNo +	aData := make([]byte, 24) +	for i := 0; i < b.N; i++ { +		// Encrypt plaintext and append to nonce +		ciphertext = aesgcm.Seal(nonce, nonce, plaintext, aData) +	} +	return ciphertext +} + +func BenchmarkEnc_Go_4k_AES256_nonce96(b *testing.B) { +	plaintext := make([]byte, 4048) +	key := make([]byte, 256/8) +	nonce := make([]byte, 96/8) +	benchmarkGoEnc(b, plaintext, key, nonce) +} + +func BenchmarkEnc_Go_4k_AES256_nonce128(b *testing.B) { +	plaintext := make([]byte, 4048) +	key := make([]byte, 256/8) +	nonce := make([]byte, 128/8) +	benchmarkGoEnc(b, plaintext, key, nonce) +} + +func BenchmarkEnc_OpenSSL_4k_AES256_nonce96(b *testing.B) { +	plaintext := make([]byte, 4048) +	key := make([]byte, 256/8) +	nonce := make([]byte, 96/8) +	benchmarkOpensslEnc(b, plaintext, key, nonce) +} + +func BenchmarkEnc_OpenSSL_4k_AES256_nonce128(b *testing.B) { +	plaintext := make([]byte, 4048) +	key := make([]byte, 256/8) +	nonce := make([]byte, 96/8) +	benchmarkOpensslEnc(b, plaintext, key, nonce) +} diff --git a/internal/nametransform/name_api.go b/internal/nametransform/name_api.go new file mode 100644 index 0000000..462e99c --- /dev/null +++ b/internal/nametransform/name_api.go @@ -0,0 +1,16 @@ +package nametransform + +import "github.com/rfjakob/gocryptfs/internal/cryptocore" + +type NameTransform struct { +	cryptoCore *cryptocore.CryptoCore +	useEME bool +	DirIVCache dirIVCache +} + +func New(c *cryptocore.CryptoCore, useEME bool) *NameTransform { +	return &NameTransform{ +		cryptoCore: c, +		useEME: useEME, +	} +} diff --git a/internal/nametransform/names_core.go b/internal/nametransform/names_core.go new file mode 100644 index 0000000..452ab45 --- /dev/null +++ b/internal/nametransform/names_core.go @@ -0,0 +1,63 @@ +package nametransform + +// Filename encryption / decryption functions + +import ( +	"crypto/aes" +	"crypto/cipher" +	"encoding/base64" +	"fmt" + +	"github.com/rfjakob/eme" +) + +// DecryptName - decrypt base64-encoded encrypted filename "cipherName" +// The used encryption is either CBC or EME, depending on "useEME". +// +// This function is exported because it allows for a very efficient readdir +// implementation (read IV once, decrypt all names using this function). +func (n *NameTransform) DecryptName(cipherName string, iv []byte) (string, error) { + +	bin, err := base64.URLEncoding.DecodeString(cipherName) +	if err != nil { +		return "", err +	} + +	if len(bin)%aes.BlockSize != 0 { +		return "", fmt.Errorf("Decoded length %d is not a multiple of the AES block size", len(bin)) +	} + +	if n.useEME { +		bin = eme.Transform(n.cryptoCore.BlockCipher, iv, bin, eme.DirectionDecrypt) +	} else { +		cbc := cipher.NewCBCDecrypter(n.cryptoCore.BlockCipher, iv) +		cbc.CryptBlocks(bin, bin) +	} + +	bin, err = unPad16(bin) +	if err != nil { +		return "", err +	} + +	plain := string(bin) +	return plain, err +} + +// encryptName - encrypt "plainName", return base64-encoded "cipherName64" +// The used encryption is either CBC or EME, depending on "useEME". +func (n *NameTransform) encryptName(plainName string, iv []byte) (cipherName64 string) { + +	bin := []byte(plainName) +	bin = pad16(bin) + +	if n.useEME { +		bin = eme.Transform(n.cryptoCore.BlockCipher, iv, bin, eme.DirectionEncrypt) +	} else { +		cbc := cipher.NewCBCEncrypter(n.cryptoCore.BlockCipher, iv) +		cbc.CryptBlocks(bin, bin) +	} + +	cipherName64 = base64.URLEncoding.EncodeToString(bin) +	return cipherName64 +} + diff --git a/internal/nametransform/names_diriv.go b/internal/nametransform/names_diriv.go new file mode 100644 index 0000000..d31a066 --- /dev/null +++ b/internal/nametransform/names_diriv.go @@ -0,0 +1,151 @@ +package nametransform + +import ( +	"fmt" +	"io/ioutil" +	"os" +	"path/filepath" +	"strings" +	"sync" + +	"github.com/rfjakob/gocryptfs/internal/toggledlog" +	"github.com/rfjakob/gocryptfs/internal/cryptocore" +) + +const ( +	// identical to AES block size +	dirIVLen       = 16 +	// dirIV is stored in this file. Exported because we have to ignore this +	// name in directory listing. +	DirIVFilename  = "gocryptfs.diriv" +) + +// A simple one-entry DirIV cache +type dirIVCache struct { +	// Invalidated? +	cleared bool +	// The DirIV +	iv []byte +	// Directory the DirIV belongs to +	dir string +	// Ecrypted version of "dir" +	translatedDir string +	// Synchronisation +	lock sync.RWMutex +} + +// lookup - fetch entry for "dir" from the cache +func (c *dirIVCache) lookup(dir string) (bool, []byte, string) { +	c.lock.RLock() +	defer c.lock.RUnlock() +	if !c.cleared && c.dir == dir { +		return true, c.iv, c.translatedDir +	} +	return false, nil, "" +} + +// store - write entry for "dir" into the caches +func (c *dirIVCache) store(dir string, iv []byte, translatedDir string) { +	c.lock.Lock() +	defer c.lock.Unlock() +	c.cleared = false +	c.iv = iv +	c.dir = dir +	c.translatedDir = translatedDir +} + +func (c *dirIVCache) Clear() { +	c.lock.Lock() +	defer c.lock.Unlock() +	c.cleared = true +} + +// readDirIV - read the "gocryptfs.diriv" file from "dir" (absolute ciphertext path) +func (be *NameTransform) ReadDirIV(dir string) (iv []byte, readErr error) { +	ivfile := filepath.Join(dir, DirIVFilename) +	toggledlog.Debug.Printf("ReadDirIV: reading %s\n", ivfile) +	iv, readErr = ioutil.ReadFile(ivfile) +	if readErr != nil { +		// The directory may have been concurrently deleted or moved. Failure to +		// read the diriv is not an error in that case. +		_, statErr := os.Stat(dir) +		if os.IsNotExist(statErr) { +			toggledlog.Debug.Printf("ReadDirIV: Dir %s was deleted under our feet", dir) +		} else { +			// This should not happen +			toggledlog.Warn.Printf("ReadDirIV: Dir exists but diriv does not: %v\n", readErr) +		} +		return nil, readErr +	} +	if len(iv) != dirIVLen { +		return nil, fmt.Errorf("ReadDirIV: Invalid length %d\n", len(iv)) +	} +	return iv, nil +} + +// WriteDirIV - create diriv file inside "dir" (absolute ciphertext path) +// This function is exported because it is used from pathfs_frontend, main, +// and also the automated tests. +func WriteDirIV(dir string) error { +	iv := cryptocore.RandBytes(dirIVLen) +	file := filepath.Join(dir, DirIVFilename) +	// 0444 permissions: the file is not secret but should not be written to +	return ioutil.WriteFile(file, iv, 0444) +} + +// EncryptPathDirIV - encrypt path using EME with DirIV +func (be *NameTransform) EncryptPathDirIV(plainPath string, rootDir string) (cipherPath string, err error) { +	// Empty string means root directory +	if plainPath == "" { +		return plainPath, nil +	} +	// Check if the DirIV is cached +	parentDir := filepath.Dir(plainPath) +	found, iv, cParentDir := be.DirIVCache.lookup(parentDir) +	if found { +		//fmt.Print("h") +		baseName := filepath.Base(plainPath) +		cBaseName := be.encryptName(baseName, iv) +		cipherPath = cParentDir + "/" + cBaseName +		return cipherPath, nil +	} +	// Walk the directory tree +	var wd = rootDir +	var encryptedNames []string +	plainNames := strings.Split(plainPath, "/") +	for _, plainName := range plainNames { +		iv, err = be.ReadDirIV(wd) +		if err != nil { +			return "", err +		} +		encryptedName := be.encryptName(plainName, iv) +		encryptedNames = append(encryptedNames, encryptedName) +		wd = filepath.Join(wd, encryptedName) +	} +	// Cache the final DirIV +	cipherPath = strings.Join(encryptedNames, "/") +	cParentDir = filepath.Dir(cipherPath) +	be.DirIVCache.store(parentDir, iv, cParentDir) +	return cipherPath, nil +} + +// DecryptPathDirIV - decrypt path using EME with DirIV +func (be *NameTransform) DecryptPathDirIV(encryptedPath string, rootDir string, eme bool) (string, error) { +	var wd = rootDir +	var plainNames []string +	encryptedNames := strings.Split(encryptedPath, "/") +	toggledlog.Debug.Printf("DecryptPathDirIV: decrypting %v\n", encryptedNames) +	for _, encryptedName := range encryptedNames { +		iv, err := be.ReadDirIV(wd) +		if err != nil { +			return "", err +		} +		plainName, err := be.DecryptName(encryptedName, iv) +		if err != nil { +			return "", err +		} +		plainNames = append(plainNames, plainName) +		wd = filepath.Join(wd, encryptedName) +	} +	return filepath.Join(plainNames...), nil +} diff --git a/internal/nametransform/names_noiv.go b/internal/nametransform/names_noiv.go new file mode 100644 index 0000000..f301e52 --- /dev/null +++ b/internal/nametransform/names_noiv.go @@ -0,0 +1,63 @@ +package nametransform + +import ( +	"strings" +) + +const ( +	OpEncrypt = iota +	OpDecrypt +) + +// DecryptPathNoIV - decrypt path using CBC without any IV. +// This function is deprecated by the the more secure DirIV variant and only retained +// for compatability with old filesystems. +func (be *NameTransform) DecryptPathNoIV(cipherPath string) (plainPath string, err error) { +	plainPath, err = be.translatePathNoIV(cipherPath, OpDecrypt) +	return plainPath, err +} + +// EncryptPathNoIV - decrypt path using CBC without any IV. +// This function is deprecated by the the more secure DirIV variant and only retained +// for compatability with old filesystems. +func (be *NameTransform) EncryptPathNoIV(plainPath string) (cipherPath string) { +	cipherPath, _ = be.translatePathNoIV(plainPath, OpEncrypt) +	return cipherPath +} + +// translatePathZeroIV - encrypt or decrypt path using CBC with an all-zero IV. +// Just splits the string on "/" and hands the parts to encryptName() / decryptName() +func (be *NameTransform) translatePathNoIV(path string, op int) (string, error) { +	var err error + +	// Empty string means root directory +	if path == "" { +		return path, err +	} + +	zeroIV := make([]byte, dirIVLen) + +	// Run operation on each path component +	var translatedParts []string +	parts := strings.Split(path, "/") +	for _, part := range parts { +		if part == "" { +			// This happens on "/foo/bar/" on the front and on the end. +			// Don't panic. +			translatedParts = append(translatedParts, "") +			continue +		} +		var newPart string +		if op == OpEncrypt { +			newPart = be.encryptName(part, zeroIV) +		} else { +			newPart, err = be.DecryptName(part, zeroIV) +			if err != nil { +				return "", err +			} +		} +		translatedParts = append(translatedParts, newPart) +	} + +	return strings.Join(translatedParts, "/"), err +} diff --git a/internal/nametransform/names_test.go b/internal/nametransform/names_test.go new file mode 100644 index 0000000..4a901be --- /dev/null +++ b/internal/nametransform/names_test.go @@ -0,0 +1,58 @@ +package nametransform + +import ( +	"bytes" +	"testing" +) + +func TestEncryptPathNoIV(t *testing.T) { +	var s []string +	s = append(s, "foo") +	s = append(s, "foo12312312312312312313123123123") +	s = append(s, "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890") + +	key := make([]byte, KEY_LEN) +	fs := NewCryptFS(key, true, false, true) + +	for _, n := range s { +		c := fs.EncryptPathNoIV(n) +		d, err := fs.DecryptPathNoIV(c) +		if err != nil { +			t.Errorf("Got error from DecryptPathNoIV: %s", err) +		} +		if d != n { +			t.Errorf("Content mismatch, n != d: n=%s c=%s d=%s", n, c, d) +		} +	} +} + +func TestPad16(t *testing.T) { +	var s [][]byte +	s = append(s, []byte("foo")) +	s = append(s, []byte("12345678901234567")) +	s = append(s, []byte("12345678901234567abcdefg")) + +	key := make([]byte, KEY_LEN) +	fs := NewCryptFS(key, true, false, true) + +	for i := range s { +		orig := s[i] +		padded := fs.pad16(orig) +		if len(padded) <= len(orig) { +			t.Errorf("Padded length not bigger than orig: %d", len(padded)) +		} +		if len(padded)%16 != 0 { +			t.Errorf("Length is not aligend: %d", len(padded)) +		} +		unpadded, err := fs.unPad16(padded) +		if err != nil { +			t.Error("unPad16 returned error:", err) +		} +		if len(unpadded) != len(orig) { +			t.Errorf("Size mismatch: orig=%d unpadded=%d", len(s[i]), len(unpadded)) +		} +		if !bytes.Equal(orig, unpadded) { +			t.Error("Content mismatch orig vs unpadded") +		} +	} +} diff --git a/internal/nametransform/pad16.go b/internal/nametransform/pad16.go new file mode 100644 index 0000000..c15160e --- /dev/null +++ b/internal/nametransform/pad16.go @@ -0,0 +1,60 @@ +package nametransform + +import ( +	"fmt" +	"crypto/aes" +	"errors" +) + +// pad16 - pad data to AES block size (=16 byte) using standard PKCS#7 padding +// https://tools.ietf.org/html/rfc5652#section-6.3 +func 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 unPad16(padded []byte) ([]byte, error) { +	oldLen := len(padded) +	if oldLen%aes.BlockSize != 0 { +		return nil, errors.New("Unaligned size") +	} +	// The last byte is always a padding byte +	padByte := padded[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, fmt.Errorf("Padding too long, padLen = %d > 16", padLen) +	} +	// All padding bytes must be identical +	for i := oldLen - padLen; i < oldLen; i++ { +		if padded[i] != padByte { +			return nil, fmt.Errorf("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 padded[0:newLen], nil +} diff --git a/internal/toggledlog/log.go b/internal/toggledlog/log.go new file mode 100644 index 0000000..4a2ad03 --- /dev/null +++ b/internal/toggledlog/log.go @@ -0,0 +1,65 @@ +package toggledlog + +import ( +	"encoding/json" +	"fmt" +	"log" +	"os" +) + +const ( +	ProgramName = "gocryptfs" +) + +func JSONDump(obj interface{}) string { +	b, err := json.MarshalIndent(obj, "", "\t") +	if err != nil { +		return err.Error() +	} else { +		return string(b) +	} +} + +// toggledLogger - a Logger than can be enabled and disabled +type toggledLogger struct { +	// Enable or disable output +	Enabled bool +	// Panic after logging a message, useful in regression tests +	PanicAfter bool +	*log.Logger +} + +func (l *toggledLogger) Printf(format string, v ...interface{}) { +	if !l.Enabled { +		return +	} +	l.Logger.Printf(format, v...) +	if l.PanicAfter { +		panic("PanicAfter: " + fmt.Sprintf(format, v...)) +	} +} +func (l *toggledLogger) Println(v ...interface{}) { +	if !l.Enabled { +		return +	} +	l.Logger.Println(v...) +	if l.PanicAfter { +		panic("PanicAfter: " + fmt.Sprintln(v...)) +	} +} + +// As defined by http://elinux.org/Debugging_by_printing#Log_Levels +// Debug messages +var Debug *toggledLogger + +// Informational message e.g. startup information +var Info *toggledLogger + +// A warning, meaning nothing serious by itself but might indicate problems +var Warn *toggledLogger + +func init() { +	Debug = &toggledLogger{false, false, log.New(os.Stdout, "", 0)} +	Info = &toggledLogger{true, false, log.New(os.Stdout, "", 0)} +	Warn = &toggledLogger{true, false, log.New(os.Stderr, "", 0)} +} diff --git a/internal/toggledlog/log_go1.4.go b/internal/toggledlog/log_go1.4.go new file mode 100644 index 0000000..4cdba44 --- /dev/null +++ b/internal/toggledlog/log_go1.4.go @@ -0,0 +1,12 @@ +// +build !go1.5 +// = go 1.4 or lower + +package toggledlog + +import ( +	"log/syslog" +) + +func (l *toggledLogger) SwitchToSyslog(p syslog.Priority) { +	Debug.Printf("Cannot switch to syslog - need Go 1.5 or higher") +} diff --git a/internal/toggledlog/log_go1.5.go b/internal/toggledlog/log_go1.5.go new file mode 100644 index 0000000..e8e71f9 --- /dev/null +++ b/internal/toggledlog/log_go1.5.go @@ -0,0 +1,17 @@ +// +build go1.5 +// = go 1.5 or higher + +package toggledlog + +import ( +	"log/syslog" +) + +func (l *toggledLogger) SwitchToSyslog(p syslog.Priority) { +	w, err := syslog.New(p, ProgramName) +	if err != nil { +		Warn.Printf("Cannot switch 0x%02x to syslog: %v", p, err) +	} else { +		l.SetOutput(w) +	} +} | 
