diff options
author | Jakob Unterwurzacher | 2016-02-06 19:20:54 +0100 |
---|---|---|
committer | Jakob Unterwurzacher | 2016-02-06 19:22:35 +0100 |
commit | 2b8cbd944149afe51fadddbd67ee4499d1d86250 (patch) | |
tree | 76361984cc4394bbb9b19ae987aeaff71fb6073b /internal | |
parent | adcfbd79a8b8bb85cbee25996ab622a05de0dbc1 (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')
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) + } +} |