aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorJakob Unterwurzacher2016-02-06 19:20:54 +0100
committerJakob Unterwurzacher2016-02-06 19:22:35 +0100
commit2b8cbd944149afe51fadddbd67ee4499d1d86250 (patch)
tree76361984cc4394bbb9b19ae987aeaff71fb6073b /internal
parentadcfbd79a8b8bb85cbee25996ab622a05de0dbc1 (diff)
Major refactoring: Split up "cryptfs" into several internal packages
"git status" for reference: deleted: cryptfs/cryptfs.go deleted: cryptfs/names_core.go modified: integration_tests/cli_test.go modified: integration_tests/helpers.go renamed: cryptfs/config_file.go -> internal/configfile/config_file.go renamed: cryptfs/config_test.go -> internal/configfile/config_test.go renamed: cryptfs/config_test/.gitignore -> internal/configfile/config_test/.gitignore renamed: cryptfs/config_test/PlaintextNames.conf -> internal/configfile/config_test/PlaintextNames.conf renamed: cryptfs/config_test/StrangeFeature.conf -> internal/configfile/config_test/StrangeFeature.conf renamed: cryptfs/config_test/v1.conf -> internal/configfile/config_test/v1.conf renamed: cryptfs/config_test/v2.conf -> internal/configfile/config_test/v2.conf renamed: cryptfs/kdf.go -> internal/configfile/kdf.go renamed: cryptfs/kdf_test.go -> internal/configfile/kdf_test.go renamed: cryptfs/cryptfs_content.go -> internal/contentenc/content.go new file: internal/contentenc/content_api.go renamed: cryptfs/content_test.go -> internal/contentenc/content_test.go renamed: cryptfs/file_header.go -> internal/contentenc/file_header.go renamed: cryptfs/intrablock.go -> internal/contentenc/intrablock.go renamed: cryptfs/address_translation.go -> internal/contentenc/offsets.go new file: internal/cryptocore/crypto_api.go renamed: cryptfs/gcm_go1.4.go -> internal/cryptocore/gcm_go1.4.go renamed: cryptfs/gcm_go1.5.go -> internal/cryptocore/gcm_go1.5.go renamed: cryptfs/nonce.go -> internal/cryptocore/nonce.go renamed: cryptfs/openssl_aead.go -> internal/cryptocore/openssl_aead.go renamed: cryptfs/openssl_benchmark.bash -> internal/cryptocore/openssl_benchmark.bash renamed: cryptfs/openssl_test.go -> internal/cryptocore/openssl_test.go new file: internal/nametransform/name_api.go new file: internal/nametransform/names_core.go renamed: cryptfs/names_diriv.go -> internal/nametransform/names_diriv.go renamed: cryptfs/names_noiv.go -> internal/nametransform/names_noiv.go renamed: cryptfs/names_test.go -> internal/nametransform/names_test.go new file: internal/nametransform/pad16.go renamed: cryptfs/log.go -> internal/toggledlog/log.go renamed: cryptfs/log_go1.4.go -> internal/toggledlog/log_go1.4.go renamed: cryptfs/log_go1.5.go -> internal/toggledlog/log_go1.5.go modified: main.go modified: masterkey.go modified: pathfs_frontend/file.go modified: pathfs_frontend/file_holes.go modified: pathfs_frontend/fs.go modified: pathfs_frontend/fs_dir.go modified: pathfs_frontend/names.go modified: test.bash
Diffstat (limited to 'internal')
-rw-r--r--internal/configfile/config_file.go195
-rw-r--r--internal/configfile/config_test.go83
-rw-r--r--internal/configfile/config_test/.gitignore1
-rw-r--r--internal/configfile/config_test/PlaintextNames.conf14
-rw-r--r--internal/configfile/config_test/StrangeFeature.conf14
-rw-r--r--internal/configfile/config_test/v1.conf11
-rw-r--r--internal/configfile/config_test/v2.conf11
-rw-r--r--internal/configfile/kdf.go57
-rw-r--r--internal/configfile/kdf_test.go60
-rw-r--r--internal/contentenc/content.go116
-rw-r--r--internal/contentenc/content_api.go31
-rw-r--r--internal/contentenc/content_test.go91
-rw-r--r--internal/contentenc/file_header.go60
-rw-r--r--internal/contentenc/intrablock.go51
-rw-r--r--internal/contentenc/offsets.go97
-rw-r--r--internal/cryptocore/crypto_api.go56
-rw-r--r--internal/cryptocore/gcm_go1.4.go22
-rw-r--r--internal/cryptocore/gcm_go1.5.go16
-rw-r--r--internal/cryptocore/nonce.go44
-rw-r--r--internal/cryptocore/openssl_aead.go100
-rwxr-xr-xinternal/cryptocore/openssl_benchmark.bash3
-rw-r--r--internal/cryptocore/openssl_test.go75
-rw-r--r--internal/nametransform/name_api.go16
-rw-r--r--internal/nametransform/names_core.go63
-rw-r--r--internal/nametransform/names_diriv.go151
-rw-r--r--internal/nametransform/names_noiv.go63
-rw-r--r--internal/nametransform/names_test.go58
-rw-r--r--internal/nametransform/pad16.go60
-rw-r--r--internal/toggledlog/log.go65
-rw-r--r--internal/toggledlog/log_go1.4.go12
-rw-r--r--internal/toggledlog/log_go1.5.go17
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)
+ }
+}