From c6dacd6f913b4c6eb7a8917af49190dce32db108 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Tue, 8 Dec 2015 16:13:29 +0100 Subject: Add EME filename encryption & enable it by default --- cryptfs/config_file.go | 20 +++--- cryptfs/config_test.go | 2 +- cryptfs/cryptfs.go | 2 - cryptfs/cryptfs_names.go | 156 ----------------------------------------------- cryptfs/filter.go | 15 ----- cryptfs/log.go | 13 ++++ cryptfs/names_core.go | 134 ++++++++++++++++++++++++++++++++++++++++ cryptfs/names_diriv.go | 31 ++++------ cryptfs/names_noiv.go | 63 +++++++++++++++++++ cryptfs/names_test.go | 11 ++-- 10 files changed, 241 insertions(+), 206 deletions(-) delete mode 100644 cryptfs/cryptfs_names.go delete mode 100644 cryptfs/filter.go create mode 100644 cryptfs/names_core.go create mode 100644 cryptfs/names_noiv.go (limited to 'cryptfs') diff --git a/cryptfs/config_file.go b/cryptfs/config_file.go index be37c60..bf1f2a0 100644 --- a/cryptfs/config_file.go +++ b/cryptfs/config_file.go @@ -12,10 +12,6 @@ const ( // The dot "." is not used in base64url (RFC4648), hence // we can never clash with an encrypted file. ConfDefaultName = "gocryptfs.conf" - // Understood Feature Flags - // Also teach isFeatureFlagKnown() about any additions - FlagPlaintextNames = "PlaintextNames" - FlagDirIV = "DirIV" ) type ConfFile struct { @@ -37,7 +33,7 @@ type ConfFile struct { // 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 { +func CreateConfFile(filename string, password string, plaintextNames bool, logN int, EMENames bool) error { var cf ConfFile cf.filename = filename @@ -50,11 +46,13 @@ func CreateConfFile(filename string, password string, plaintextNames bool, logN // Set defaults cf.Version = HEADER_CURRENT_VERSION - cf.FeatureFlags = []string{FlagDirIV} // Set values chosen by the user 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 @@ -157,10 +155,18 @@ func (cf *ConfFile) WriteFile() error { return nil } +const ( + // Understood Feature Flags. + // Also teach isFeatureFlagKnown() about any additions + FlagPlaintextNames = "PlaintextNames" + FlagDirIV = "DirIV" + FlagEMENames = "EMENames" +) + // Verify that we understand a feature flag func (cf *ConfFile) isFeatureFlagKnown(flag string) bool { switch flag { - case FlagPlaintextNames, FlagDirIV: + case FlagPlaintextNames, FlagDirIV, FlagEMENames: return true default: return false diff --git a/cryptfs/config_test.go b/cryptfs/config_test.go index 1c5a375..01c0d71 100644 --- a/cryptfs/config_test.go +++ b/cryptfs/config_test.go @@ -59,7 +59,7 @@ func TestLoadV2StrangeFeature(t *testing.T) { } func TestCreateConfFile(t *testing.T) { - err := CreateConfFile("config_test/tmp.conf", "test", false, 0) + err := CreateConfFile("config_test/tmp.conf", "test", false, 10, true) if err != nil { t.Fatal(err) } diff --git a/cryptfs/cryptfs.go b/cryptfs/cryptfs.go index df04973..5832e36 100644 --- a/cryptfs/cryptfs.go +++ b/cryptfs/cryptfs.go @@ -25,7 +25,6 @@ type CryptFS struct { cipherBS uint64 // Stores an all-zero block of size cipherBS allZeroBlock []byte - plaintextNames bool // DirIV cache for filename encryption DirIVCacheEnc DirIVCache } @@ -59,7 +58,6 @@ func NewCryptFS(key []byte, useOpenssl bool, plaintextNames bool) *CryptFS { plainBS: DEFAULT_PLAINBS, cipherBS: uint64(cipherBS), allZeroBlock: make([]byte, cipherBS), - plaintextNames: plaintextNames, } } diff --git a/cryptfs/cryptfs_names.go b/cryptfs/cryptfs_names.go deleted file mode 100644 index 8f8486b..0000000 --- a/cryptfs/cryptfs_names.go +++ /dev/null @@ -1,156 +0,0 @@ -package cryptfs - -// Filename encryption / decryption function - -import ( - "crypto/aes" - "crypto/cipher" - "encoding/base64" - "errors" - "fmt" - "strings" -) - -const ( - OpEncrypt = iota - OpDecrypt -) - -// DecryptName - decrypt base64-encoded encrypted filename "cipherName" -func (be *CryptFS) DecryptName(cipherName string, iv []byte) (string, error) { - - // Make sure relative symlinks still work after encryption - // by passing these through unchanged - if cipherName == "." || cipherName == ".." { - return cipherName, nil - } - - 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)) - } - - cbc := cipher.NewCBCDecrypter(be.blockCipher, iv) - cbc.CryptBlocks(bin, bin) - - bin, err = be.unPad16(bin) - if err != nil { - return "", err - } - - plain := string(bin) - return plain, err -} - -// EncryptName - encrypt filename -func (be *CryptFS) encryptName(plainName string, iv []byte) string { - - // Make sure relative symlinks still work after encryption - // by passing these trough unchanged - if plainName == "." || plainName == ".." { - return plainName - } - - bin := []byte(plainName) - bin = be.pad16(bin) - - cbc := cipher.NewCBCEncrypter(be.blockCipher, iv) - cbc.CryptBlocks(bin, bin) - - cipherName64 := base64.URLEncoding.EncodeToString(bin) - return cipherName64 -} - -// TranslatePathZeroIV - encrypt or decrypt path using CBC with a constant all-zero IV. -// Just splits the string on "/" and hands the parts to encryptName() / decryptName() -func (be *CryptFS) TranslatePathZeroIV(path string, op int) (string, error) { - var err error - - // Empty string means root directory - if path == "" { - return path, err - } - - zeroIV := make([]byte, DIRIV_LEN) - - // 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 -} - -// pad16 - pad filename to 16 byte blocks using standard PKCS#7 padding -// https://tools.ietf.org/html/rfc5652#section-6.3 -func (be *CryptFS) pad16(orig []byte) (padded []byte) { - oldLen := len(orig) - if oldLen == 0 { - panic("Padding zero-length string makes no sense") - } - padLen := aes.BlockSize - oldLen%aes.BlockSize - if padLen == 0 { - padLen = aes.BlockSize - } - newLen := oldLen + padLen - padded = make([]byte, newLen) - copy(padded, orig) - padByte := byte(padLen) - for i := oldLen; i < newLen; i++ { - padded[i] = padByte - } - return padded -} - -// unPad16 - remove padding -func (be *CryptFS) unPad16(orig []byte) ([]byte, error) { - oldLen := len(orig) - if oldLen%aes.BlockSize != 0 { - return nil, errors.New("Unaligned size") - } - // The last byte is always a padding byte - padByte := orig[oldLen-1] - // The padding byte's value is the padding length - padLen := int(padByte) - // Padding must be at least 1 byte - if padLen <= 0 { - return nil, errors.New("Padding cannot be zero-length") - } - // Larger paddings make no sense - if padLen > aes.BlockSize { - return nil, errors.New("Padding cannot be larger than 16") - } - // All padding bytes must be identical - for i := oldLen - padLen; i < oldLen; i++ { - if orig[i] != padByte { - return nil, errors.New(fmt.Sprintf("Padding byte at i=%d is invalid", i)) - } - } - newLen := oldLen - padLen - // Padding an empty string makes no sense - if newLen == 0 { - return nil, errors.New("Unpadded length is zero") - } - return orig[0:newLen], nil -} diff --git a/cryptfs/filter.go b/cryptfs/filter.go deleted file mode 100644 index f80889d..0000000 --- a/cryptfs/filter.go +++ /dev/null @@ -1,15 +0,0 @@ -package cryptfs - -// IsFiltered - check if "path" should be forbidden -// -// Used to prevent name clashes with gocryptfs.conf -// when file names are not encrypted -func (be *CryptFS) IsFiltered(path string) bool { - // gocryptfs.conf in the root directory is forbidden - if be.plaintextNames == true && path == ConfDefaultName { - Warn.Printf("The name /%s is reserved when -plaintextnames is used\n", - ConfDefaultName) - return true - } - return false -} diff --git a/cryptfs/log.go b/cryptfs/log.go index 64dc80e..a7fe579 100644 --- a/cryptfs/log.go +++ b/cryptfs/log.go @@ -3,6 +3,7 @@ package cryptfs import ( "fmt" "strings" + "encoding/json" ) type logChannel struct { @@ -26,6 +27,18 @@ func (l *logChannel) Dump(d []byte) { fmt.Println(strings.Replace(s, "\000", "\\0", -1)) } +func (l *logChannel) JSONDump(obj interface{}) { + if !l.enabled { + return + } + b, err := json.MarshalIndent(obj, "", "\t") + if err != nil { + fmt.Println(err) + } else { + fmt.Println(string(b)) + } +} + func (l *logChannel) Enable() { l.enabled = true } diff --git a/cryptfs/names_core.go b/cryptfs/names_core.go new file mode 100644 index 0000000..0f2e5b3 --- /dev/null +++ b/cryptfs/names_core.go @@ -0,0 +1,134 @@ +package cryptfs + +// Filename encryption / decryption functions + +import ( + "crypto/aes" + "crypto/cipher" + "encoding/base64" + "errors" + "fmt" + + "github.com/rfjakob/eme" +) + +// DecryptName - decrypt base64-encoded encrypted filename "cipherName" +// The used encryption is either CBC or EME, depending on the "EMENames" argument. +// +// This function is exported because it allows for a very efficient readdir +// implementation (read IV once, decrypt all names using this function). +func (be *CryptFS) DecryptName(cipherName string, iv []byte, EMENames bool) (string, error) { + return be.decryptName(cipherName, iv, EMENames) +} + +// decryptName - decrypt base64-encoded encrypted filename "cipherName". +// The used encryption is either CBC or EME, depending on the "EMENames" argument. +func (be *CryptFS) decryptName(cipherName string, iv []byte, EMENames bool) (string, error) { + + // Make sure relative symlinks still work after encryption + // by passing these through unchanged + if cipherName == "." || cipherName == ".." { + return cipherName, nil + } + + 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 EMENames { + bin = eme.Transform(be.blockCipher, iv, bin, eme.DirectionDecrypt) + } else { + cbc := cipher.NewCBCDecrypter(be.blockCipher, iv) + cbc.CryptBlocks(bin, bin) + } + + bin, err = be.unPad16(bin) + if err != nil { + return "", err + } + + plain := string(bin) + return plain, err +} + +// encryptName - encrypt "plainName", return base64-encoded "cipherName64" +// The used encryption is either CBC or EME, depending on the "EMENames" argument. +func (be *CryptFS) encryptName(plainName string, iv []byte, EMENames bool) (cipherName64 string) { + + // Make sure relative symlinks still work after encryption + // by passing these trough unchanged + if plainName == "." || plainName == ".." { + return plainName + } + + bin := []byte(plainName) + bin = be.pad16(bin) + + if EMENames { + bin = eme.Transform(be.blockCipher, iv, bin, eme.DirectionEncrypt) + } else { + cbc := cipher.NewCBCEncrypter(be.blockCipher, iv) + cbc.CryptBlocks(bin, bin) + } + + cipherName64 = base64.URLEncoding.EncodeToString(bin) + return cipherName64 +} + +// pad16 - pad filename to 16 byte blocks using standard PKCS#7 padding +// https://tools.ietf.org/html/rfc5652#section-6.3 +func (be *CryptFS) pad16(orig []byte) (padded []byte) { + oldLen := len(orig) + if oldLen == 0 { + panic("Padding zero-length string makes no sense") + } + padLen := aes.BlockSize - oldLen%aes.BlockSize + if padLen == 0 { + padLen = aes.BlockSize + } + newLen := oldLen + padLen + padded = make([]byte, newLen) + copy(padded, orig) + padByte := byte(padLen) + for i := oldLen; i < newLen; i++ { + padded[i] = padByte + } + return padded +} + +// unPad16 - remove padding +func (be *CryptFS) unPad16(orig []byte) ([]byte, error) { + oldLen := len(orig) + if oldLen%aes.BlockSize != 0 { + return nil, errors.New("Unaligned size") + } + // The last byte is always a padding byte + padByte := orig[oldLen-1] + // The padding byte's value is the padding length + padLen := int(padByte) + // Padding must be at least 1 byte + if padLen <= 0 { + return nil, errors.New("Padding cannot be zero-length") + } + // Larger paddings make no sense + if padLen > aes.BlockSize { + return nil, errors.New("Padding cannot be larger than 16") + } + // All padding bytes must be identical + for i := oldLen - padLen; i < oldLen; i++ { + if orig[i] != padByte { + return nil, errors.New(fmt.Sprintf("Padding byte at i=%d is invalid", i)) + } + } + newLen := oldLen - padLen + // Padding an empty string makes no sense + if newLen == 0 { + return nil, errors.New("Unpadded length is zero") + } + return orig[0:newLen], nil +} diff --git a/cryptfs/names_diriv.go b/cryptfs/names_diriv.go index 035eac1..2e2429e 100644 --- a/cryptfs/names_diriv.go +++ b/cryptfs/names_diriv.go @@ -73,11 +73,8 @@ func WriteDirIV(dir string) error { return ioutil.WriteFile(file, iv, 0444) } -// EncryptPathDirIV - encrypt path using CBC with DirIV -func (be *CryptFS) EncryptPathDirIV(plainPath string, rootDir string) (string, error) { - if be.plaintextNames { - return plainPath, nil - } +// EncryptPathDirIV - encrypt path using CBC or EME with DirIV +func (be *CryptFS) EncryptPathDirIV(plainPath string, rootDir string, eme bool) (cipherPath string, err error) { // Empty string means root directory if plainPath == "" { return plainPath, nil @@ -88,36 +85,32 @@ func (be *CryptFS) EncryptPathDirIV(plainPath string, rootDir string) (string, e if found { //fmt.Print("h") baseName := filepath.Base(plainPath) - cBaseName := be.encryptName(baseName, iv) - cPath := cParentDir + "/" + cBaseName - return cPath, nil + cBaseName := be.encryptName(baseName, iv, eme) + cipherPath = cParentDir + "/" + cBaseName + return cipherPath, nil } // Walk the directory tree var wd = rootDir var encryptedNames []string - var err error plainNames := strings.Split(plainPath, "/") for _, plainName := range plainNames { iv, err = be.ReadDirIV(wd) if err != nil { return "", err } - encryptedName := be.encryptName(plainName, iv) + encryptedName := be.encryptName(plainName, iv, eme) encryptedNames = append(encryptedNames, encryptedName) wd = filepath.Join(wd, encryptedName) } // Cache the final DirIV - cPath := strings.Join(encryptedNames, "/") - cParentDir = filepath.Dir(cPath) + cipherPath = strings.Join(encryptedNames, "/") + cParentDir = filepath.Dir(cipherPath) be.DirIVCacheEnc.store(parentDir, iv, cParentDir) - return cPath, nil + return cipherPath, nil } -// DecryptPathDirIV - encrypt path using CBC with DirIV -func (be *CryptFS) DecryptPathDirIV(encryptedPath string, rootDir string) (string, error) { - if be.plaintextNames { - return encryptedPath, nil - } +// DecryptPathDirIV - encrypt path using CBC or EME with DirIV +func (be *CryptFS) DecryptPathDirIV(encryptedPath string, rootDir string, eme bool) (string, error) { var wd = rootDir var plainNames []string encryptedNames := strings.Split(encryptedPath, "/") @@ -127,7 +120,7 @@ func (be *CryptFS) DecryptPathDirIV(encryptedPath string, rootDir string) (strin if err != nil { return "", err } - plainName, err := be.DecryptName(encryptedName, iv) + plainName, err := be.decryptName(encryptedName, iv, eme) if err != nil { return "", err } diff --git a/cryptfs/names_noiv.go b/cryptfs/names_noiv.go new file mode 100644 index 0000000..7eed4b8 --- /dev/null +++ b/cryptfs/names_noiv.go @@ -0,0 +1,63 @@ +package cryptfs + +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 *CryptFS) 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 *CryptFS) 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 *CryptFS) translatePathNoIV(path string, op int) (string, error) { + var err error + + // Empty string means root directory + if path == "" { + return path, err + } + + zeroIV := make([]byte, DIRIV_LEN) + + // 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, false) + } else { + newPart, err = be.decryptName(part, zeroIV, false) + if err != nil { + return "", err + } + } + translatedParts = append(translatedParts, newPart) + } + + return strings.Join(translatedParts, "/"), err +} diff --git a/cryptfs/names_test.go b/cryptfs/names_test.go index 1ad3391..6dffae3 100644 --- a/cryptfs/names_test.go +++ b/cryptfs/names_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -func TestTranslatePath(t *testing.T) { +func TestEncryptPathNoIV(t *testing.T) { var s []string s = append(s, "foo") s = append(s, "foo12312312312312312313123123123") @@ -15,15 +15,14 @@ func TestTranslatePath(t *testing.T) { fs := NewCryptFS(key, true, false) for _, n := range s { - c, err := fs.TranslatePathZeroIV(n, OpEncrypt) - d, err := fs.TranslatePathZeroIV(c, OpDecrypt) + c := fs.EncryptPathNoIV(n) + d, err := fs.DecryptPathNoIV(c) if err != nil { - t.Errorf("Got error from DecryptName: %s", err) + t.Errorf("Got error from DecryptPathNoIV: %s", err) } if d != n { - t.Errorf("Content mismatch, n=\"%s\" d=\"%s\"", n, d) + t.Errorf("Content mismatch, n != d: n=%s c=%s d=%s", n, c, d) } - //fmt.Printf("n=%s c=%s d=%s\n", n, c, d) } } -- cgit v1.2.3