aboutsummaryrefslogtreecommitdiff
path: root/internal/nametransform
diff options
context:
space:
mode:
Diffstat (limited to 'internal/nametransform')
-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
6 files changed, 411 insertions, 0 deletions
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
+}