Compare commits
28 Commits
98ab59c96e
...
2a9d70d48f
Author | SHA1 | Date |
---|---|---|
Jakob Unterwurzacher | 2a9d70d48f | |
Jakob Unterwurzacher | d6c8d892ff | |
Jakob Unterwurzacher | fe616ddad5 | |
Jakob Unterwurzacher | 49507ea869 | |
Jakob Unterwurzacher | ad3eeaedc5 | |
Jakob Unterwurzacher | 446c3d7e93 | |
Jakob Unterwurzacher | 4fd95b718b | |
Jakob Unterwurzacher | 5306fc345b | |
Jakob Unterwurzacher | 1f29542b39 | |
Jakob Unterwurzacher | 45648e567a | |
Jakob Unterwurzacher | f9f4bd214f | |
Jakob Unterwurzacher | ee59b5269b | |
Jakob Unterwurzacher | cbd5e8ba01 | |
Jakob Unterwurzacher | 389aba6a6b | |
Jakob Unterwurzacher | 84e702126a | |
Jakob Unterwurzacher | 05b813f202 | |
Jakob Unterwurzacher | 689b74835b | |
Jakob Unterwurzacher | 2efef1e270 | |
Jakob Unterwurzacher | e244b51491 | |
Jakob Unterwurzacher | 6b0e63c1a8 | |
Jakob Unterwurzacher | c5d8fa83ae | |
Jakob Unterwurzacher | 203e65066f | |
Jakob Unterwurzacher | 50630e9f3d | |
DerDonut | a611810ff4 | |
Jakob Unterwurzacher | cdddd1d711 | |
Jakob Unterwurzacher | 9ac77d42d1 | |
Marcel Bochtler | 2e738efe86 | |
Marcel Bochtler | 7889346947 |
|
@ -152,6 +152,31 @@ other users, subject to file permission checking. Only works if
|
|||
user_allow_other is set in /etc/fuse.conf. This option is equivalent to
|
||||
"allow_other" plus "default_permissions" described in fuse(8).
|
||||
|
||||
#### -badname string
|
||||
When gocryptfs encounters a "bad" file name (cannot be decrypted or decrypts
|
||||
to garbage), a warning is logged and the file is hidden from the
|
||||
plaintext view.
|
||||
|
||||
With the `-badname` option, you can select "bad" file names that should
|
||||
still be shown in the plaintext view instead of hiding them. Bad files
|
||||
will get ` GOCRYPTFS_BAD_NAME` appended to their name.
|
||||
|
||||
Glob pattern. Can be passed multiple times for multiple patterns.
|
||||
|
||||
Examples:
|
||||
|
||||
Dropbox sync conflicts:
|
||||
|
||||
-badname '*conflicted copy*'
|
||||
|
||||
Syncthing sync conflicts:
|
||||
|
||||
-badname '*.sync-conflict*'
|
||||
|
||||
Show all invalid filenames:
|
||||
|
||||
-badname '*'
|
||||
|
||||
#### -ctlsock string
|
||||
Create a control socket at the specified location. The socket can be
|
||||
used to decrypt and encrypt paths inside the filesystem. When using
|
||||
|
|
|
@ -72,6 +72,7 @@ v2.0-beta2-36-g6aae2aa 505 1000 16.1 8.2 6.3 7.7
|
|||
v2.0-beta2-37-g24d5d39 558 1000 12.3 6.4 4.4 2.8 fs: add initial dirfd caching
|
||||
v2.0-beta2-42-g4a07d65 549 1000 8.2 4.7 1.8 2.4 fusefrontend: make dirCache work for "node itself"
|
||||
v2.0 420 1000 8.5 4.5 1.8 2.3 go1.16.5, Linux 5.11.21
|
||||
v2.0.1-28-g49507ea 471 991 8.6 4.5 1.7 2.2
|
||||
|
||||
Results for EncFS for comparison (benchmark.bash -encfs):
|
||||
|
||||
|
|
14
README.md
14
README.md
|
@ -42,10 +42,10 @@ Platforms
|
|||
|
||||
Linux is gocryptfs' native platform.
|
||||
|
||||
Beta-quality Mac OS X support is available, which means most things work
|
||||
Beta-quality macOS support is available, which means most things work
|
||||
fine but you may hit an occasional problem. Check out
|
||||
[ticket #15](https://github.com/rfjakob/gocryptfs/issues/15) for the history
|
||||
of Mac OS X support but please create a new ticket if you hit a problem.
|
||||
of macOS support but please create a new ticket if you hit a problem.
|
||||
|
||||
For Windows, an independent C++ reimplementation can be found here:
|
||||
[cppcryptfs](https://github.com/bailey27/cppcryptfs)
|
||||
|
@ -62,11 +62,16 @@ On Debian, gocryptfs is available as a deb package:
|
|||
apt install gocryptfs
|
||||
```
|
||||
|
||||
On Mac OS X, gocryptfs is available as a Homebrew formula:
|
||||
On macOS, gocryptfs is available as a Homebrew formula:
|
||||
```bash
|
||||
brew install gocryptfs
|
||||
```
|
||||
|
||||
Alternatively, gocryptfs is also available via [MacPorts](https://www.macports.org/) on macOS:
|
||||
```bash
|
||||
sudo port install gocryptfs
|
||||
```
|
||||
|
||||
On Fedora, gocryptfs is available as an rpm package:
|
||||
```bash
|
||||
sudo dnf install gocryptfs
|
||||
|
@ -203,6 +208,9 @@ RM: 2,367
|
|||
Changelog
|
||||
---------
|
||||
|
||||
v2.1 (IN PROGRESS)
|
||||
* fido2: do not request PIN on `gocryptfs -init` ([#571](https://github.com/rfjakob/gocryptfs/issues/571))
|
||||
|
||||
v2.0.1, 2021-06-07
|
||||
* Fix symlink creation reporting the wrong size, causing git to report it as modified
|
||||
([#574](https://github.com/rfjakob/gocryptfs/issues/574))
|
||||
|
|
10
cli_args.go
10
cli_args.go
|
@ -10,6 +10,7 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -290,6 +291,15 @@ func parseCliOpts() (args argContainer) {
|
|||
tlog.Fatal.Printf("Idle timeout cannot be less than 0")
|
||||
os.Exit(exitcodes.Usage)
|
||||
}
|
||||
// Make sure all badname patterns are valid
|
||||
for _, pattern := range args.badname {
|
||||
_, err := filepath.Match(pattern, "")
|
||||
if err != nil {
|
||||
tlog.Fatal.Printf("-badname: invalid pattern %q supplied", pattern)
|
||||
os.Exit(exitcodes.Usage)
|
||||
}
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Find out the maximum supported filename length and print it.
|
||||
#
|
||||
# Part of the gocryptfs test suite
|
||||
# https://nuetzlich.net/gocryptfs/
|
||||
|
||||
set -eu
|
||||
MYNAME=$(basename "$0")
|
||||
|
||||
if [[ $# -ne 1 || $1 == -* ]]; then
|
||||
echo "Usage: $MYNAME DIRECTORY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Only show live progress if connected to a termial
|
||||
# https://stackoverflow.com/a/911213/1380267
|
||||
INTERACTIVE=0
|
||||
if [[ -t 1 ]] ; then
|
||||
INTERACTIVE=1
|
||||
fi
|
||||
|
||||
cd "$1"
|
||||
echo "Testing $PWD"
|
||||
|
||||
echo -n " Maximum filename length: "
|
||||
# Add one character at a time until we hit an error
|
||||
NAME=""
|
||||
while true ; do
|
||||
NEXT="${NAME}x"
|
||||
if [[ -e $NEXT ]]; then
|
||||
echo "error: file $PWD/$NEXT already exists"
|
||||
exit 1
|
||||
fi
|
||||
echo -n 2> /dev/null > $NEXT || break
|
||||
rm $NEXT
|
||||
NAME="$NEXT"
|
||||
done
|
||||
echo "${#NAME}"
|
||||
|
||||
# Set to 0 if undefined
|
||||
: ${QUICK:=0}
|
||||
|
||||
if [[ $QUICK -ne 1 ]]; then
|
||||
echo -n " Maximum dirname length: "
|
||||
# Add one character at a time until we hit an error
|
||||
NAME=""
|
||||
while true ; do
|
||||
NEXT="${NAME}x"
|
||||
mkdir $NEXT 2> /dev/null || break
|
||||
rmdir $NEXT
|
||||
NAME="$NEXT"
|
||||
done
|
||||
MAX_DIRNAME=${#NAME}
|
||||
echo "${#NAME}"
|
||||
fi
|
||||
|
||||
if [[ $QUICK -eq 1 ]]; then
|
||||
CHARS_TODO=100
|
||||
else
|
||||
CHARS_TODO="1 10 100 $MAX_DIRNAME"
|
||||
fi
|
||||
|
||||
for CHARS_PER_SUBDIR in $CHARS_TODO ; do
|
||||
echo -n " Maximum path length with $(printf %3d $CHARS_PER_SUBDIR) chars per subdir: "
|
||||
if [[ $INTERACTIVE -eq 1 ]] ; then
|
||||
echo -n " "
|
||||
fi
|
||||
# Trick from https://stackoverflow.com/a/5349842/1380267
|
||||
SUBDIR=$(printf 'x%.0s' $(seq 1 $CHARS_PER_SUBDIR))
|
||||
mkdir "$SUBDIR"
|
||||
P=$SUBDIR
|
||||
# Create a deep path, one $SUBDIR at a time, until we hit an error
|
||||
while true ; do
|
||||
NEXT="$P/$SUBDIR"
|
||||
mkdir "$NEXT" 2> /dev/null || break
|
||||
P=$NEXT
|
||||
if [[ $INTERACTIVE -eq 1 ]] ; then
|
||||
echo -n -e "\b\b\b\b"
|
||||
printf %4d ${#P}
|
||||
fi
|
||||
done
|
||||
# Then add one character at a time until we hit an error
|
||||
NAME=""
|
||||
while true ; do
|
||||
NEXT="${NAME}x"
|
||||
touch "$P/$NEXT" 2> /dev/null || break
|
||||
NAME=$NEXT
|
||||
done
|
||||
if [[ $NAME != "" ]] ; then
|
||||
P=$P/$NAME
|
||||
fi
|
||||
if [[ $INTERACTIVE -eq 1 ]] ; then
|
||||
echo -n -e "\b\b\b\b"
|
||||
fi
|
||||
printf %4d ${#P}
|
||||
echo
|
||||
rm -R "$SUBDIR"
|
||||
done
|
|
@ -56,7 +56,7 @@ type ConfFile struct {
|
|||
// stored in the superblock.
|
||||
FeatureFlags []string
|
||||
// FIDO2 parameters
|
||||
FIDO2 FIDO2Params
|
||||
FIDO2 *FIDO2Params `json:",omitempty"`
|
||||
// Filename is the name of the config file. Not exported to JSON.
|
||||
filename string
|
||||
}
|
||||
|
@ -102,8 +102,10 @@ func Create(filename string, password []byte, plaintextNames bool,
|
|||
}
|
||||
if len(fido2CredentialID) > 0 {
|
||||
cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagFIDO2])
|
||||
cf.FIDO2.CredentialID = fido2CredentialID
|
||||
cf.FIDO2.HMACSalt = fido2HmacSalt
|
||||
cf.FIDO2 = &FIDO2Params{
|
||||
CredentialID: fido2CredentialID,
|
||||
HMACSalt: fido2HmacSalt,
|
||||
}
|
||||
}
|
||||
{
|
||||
// Generate new random master key
|
||||
|
|
|
@ -73,6 +73,9 @@ type ContentEnc struct {
|
|||
|
||||
// New returns an initialized ContentEnc instance.
|
||||
func New(cc *cryptocore.CryptoCore, plainBS uint64, forceDecode bool) *ContentEnc {
|
||||
tlog.Debug.Printf("contentenc.New: plainBS=%d, forceDecode=%v",
|
||||
plainBS, forceDecode)
|
||||
|
||||
if fuse.MAX_KERNEL_WRITE%plainBS != 0 {
|
||||
log.Panicf("unaligned MAX_KERNEL_WRITE=%d", fuse.MAX_KERNEL_WRITE)
|
||||
}
|
||||
|
|
|
@ -36,6 +36,19 @@ const (
|
|||
BackendAESSIV AEADTypeEnum = 5
|
||||
)
|
||||
|
||||
func (a AEADTypeEnum) String() string {
|
||||
switch a {
|
||||
case BackendOpenSSL:
|
||||
return "BackendOpenSSL"
|
||||
case BackendGoGCM:
|
||||
return "BackendGoGCM"
|
||||
case BackendAESSIV:
|
||||
return "BackendAESSIV"
|
||||
default:
|
||||
return fmt.Sprintf("%d", a)
|
||||
}
|
||||
}
|
||||
|
||||
// CryptoCore is the low level crypto implementation.
|
||||
type CryptoCore struct {
|
||||
// EME is used for filename encryption.
|
||||
|
@ -58,6 +71,9 @@ type CryptoCore struct {
|
|||
// Note: "key" is either the scrypt hash of the password (when decrypting
|
||||
// a config file) or the masterkey (when finally mounting the filesystem).
|
||||
func New(key []byte, aeadType AEADTypeEnum, IVBitLen int, useHKDF bool, forceDecode bool) *CryptoCore {
|
||||
tlog.Debug.Printf("cryptocore.New: key=%d bytes, aeadType=%v, IVBitLen=%d, useHKDF=%v, forceDecode=%v",
|
||||
len(key), aeadType, IVBitLen, useHKDF, forceDecode)
|
||||
|
||||
if len(key) != KeyLen {
|
||||
log.Panic(fmt.Sprintf("Unsupported key length %d", len(key)))
|
||||
}
|
||||
|
|
|
@ -17,11 +17,22 @@ import (
|
|||
type fidoCommand int
|
||||
|
||||
const (
|
||||
cred fidoCommand = iota
|
||||
assert fidoCommand = iota
|
||||
assertWithPIN fidoCommand = iota
|
||||
cred fidoCommand = iota
|
||||
assert fidoCommand = iota
|
||||
)
|
||||
|
||||
// String pretty-prints for debug output
|
||||
func (fc fidoCommand) String() string {
|
||||
switch fc {
|
||||
case cred:
|
||||
return "cred"
|
||||
case assert:
|
||||
return "assert"
|
||||
default:
|
||||
return fmt.Sprintf("%d", fc)
|
||||
}
|
||||
}
|
||||
|
||||
const relyingPartyID = "gocryptfs"
|
||||
|
||||
func callFidoCommand(command fidoCommand, device string, stdin []string) ([]string, error) {
|
||||
|
@ -31,10 +42,8 @@ func callFidoCommand(command fidoCommand, device string, stdin []string) ([]stri
|
|||
cmd = exec.Command("fido2-cred", "-M", "-h", "-v", device)
|
||||
case assert:
|
||||
cmd = exec.Command("fido2-assert", "-G", "-h", device)
|
||||
case assertWithPIN:
|
||||
cmd = exec.Command("fido2-assert", "-G", "-h", "-v", device)
|
||||
}
|
||||
tlog.Debug.Printf("callFidoCommand: executing %q with args %q", cmd.Path, cmd.Args)
|
||||
tlog.Debug.Printf("callFidoCommand %s: executing %q with args %q", command, cmd.Path, cmd.Args)
|
||||
cmd.Stderr = os.Stderr
|
||||
in, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
|
@ -78,15 +87,11 @@ func Secret(device string, credentialID []byte, salt []byte) (secret []byte) {
|
|||
crid := base64.StdEncoding.EncodeToString(credentialID)
|
||||
hmacsalt := base64.StdEncoding.EncodeToString(salt)
|
||||
stdin := []string{cdh, relyingPartyID, crid, hmacsalt}
|
||||
// try asserting without PIN first
|
||||
// call fido2-assert
|
||||
out, err := callFidoCommand(assert, device, stdin)
|
||||
if err != nil {
|
||||
// if that fails, let's assert with PIN
|
||||
out, err = callFidoCommand(assertWithPIN, device, stdin)
|
||||
if err != nil {
|
||||
tlog.Fatal.Println(err)
|
||||
os.Exit(exitcodes.FIDO2Error)
|
||||
}
|
||||
tlog.Fatal.Println(err)
|
||||
os.Exit(exitcodes.FIDO2Error)
|
||||
}
|
||||
secret, err = base64.StdEncoding.DecodeString(out[4])
|
||||
if err != nil {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package fusefrontend
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
@ -18,30 +17,46 @@ var _ ctlsocksrv.Interface = &RootNode{} // Verify that interface is implemented
|
|||
// EncryptPath implements ctlsock.Backend
|
||||
//
|
||||
// Symlink-safe through openBackingDir().
|
||||
func (rn *RootNode) EncryptPath(plainPath string) (string, error) {
|
||||
if plainPath == "" {
|
||||
// Empty string gets encrypted as empty string
|
||||
func (rn *RootNode) EncryptPath(plainPath string) (cipherPath string, err error) {
|
||||
if rn.args.PlaintextNames || plainPath == "" {
|
||||
return plainPath, nil
|
||||
}
|
||||
if rn.args.PlaintextNames {
|
||||
return plainPath, nil
|
||||
|
||||
dirfd, _, errno := rn.prepareAtSyscallMyself()
|
||||
if errno != 0 {
|
||||
return "", errno
|
||||
}
|
||||
// Encrypt path level by level using openBackingDir. Pretty inefficient,
|
||||
// but does not matter here.
|
||||
defer syscall.Close(dirfd)
|
||||
|
||||
// Encrypt path level by level
|
||||
parts := strings.Split(plainPath, "/")
|
||||
wd := ""
|
||||
cPath := ""
|
||||
for _, part := range parts {
|
||||
wd = filepath.Join(wd, part)
|
||||
dirfd, cName, err := rn.openBackingDir(wd)
|
||||
wd := dirfd
|
||||
for i, part := range parts {
|
||||
dirIV, err := nametransform.ReadDirIVAt(wd)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
syscall.Close(dirfd)
|
||||
cPath = filepath.Join(cPath, cName)
|
||||
cPart, err := rn.nameTransform.EncryptAndHashName(part, dirIV)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
cipherPath = filepath.Join(cipherPath, cPart)
|
||||
// Last path component? We are done.
|
||||
if i == len(parts)-1 {
|
||||
break
|
||||
}
|
||||
// Descend into next directory
|
||||
wd, err = syscallcompat.Openat(wd, cPart, syscall.O_NOFOLLOW|syscall.O_DIRECTORY|syscallcompat.O_PATH, 0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Yes this is somewhat wasteful in terms of used file descriptors:
|
||||
// we keep them all open until the function returns. But it is simple
|
||||
// and reliable.
|
||||
defer syscall.Close(wd)
|
||||
}
|
||||
tlog.Debug.Printf("encryptPath '%s' -> '%s'", plainPath, cPath)
|
||||
return cPath, nil
|
||||
tlog.Debug.Printf("EncryptPath %q -> %q", plainPath, cipherPath)
|
||||
return cipherPath, nil
|
||||
}
|
||||
|
||||
// DecryptPath implements ctlsock.Backend
|
||||
|
@ -49,40 +64,33 @@ func (rn *RootNode) EncryptPath(plainPath string) (string, error) {
|
|||
// DecryptPath is symlink-safe because openBackingDir() and decryptPathAt()
|
||||
// are symlink-safe.
|
||||
func (rn *RootNode) DecryptPath(cipherPath string) (plainPath string, err error) {
|
||||
dirfd, _, err := rn.openBackingDir("")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer syscall.Close(dirfd)
|
||||
return rn.decryptPathAt(dirfd, cipherPath)
|
||||
}
|
||||
|
||||
// decryptPathAt decrypts a ciphertext path relative to dirfd.
|
||||
//
|
||||
// Symlink-safe through ReadDirIVAt() and ReadLongNameAt().
|
||||
func (rn *RootNode) decryptPathAt(dirfd int, cipherPath string) (plainPath string, err error) {
|
||||
if rn.args.PlaintextNames || cipherPath == "" {
|
||||
return cipherPath, nil
|
||||
}
|
||||
|
||||
dirfd, _, errno := rn.prepareAtSyscallMyself()
|
||||
if errno != 0 {
|
||||
return "", errno
|
||||
}
|
||||
defer syscall.Close(dirfd)
|
||||
|
||||
// Decrypt path level by level
|
||||
parts := strings.Split(cipherPath, "/")
|
||||
wd := dirfd
|
||||
for i, part := range parts {
|
||||
dirIV, err := nametransform.ReadDirIVAt(wd)
|
||||
if err != nil {
|
||||
fmt.Printf("ReadDirIV: %v\n", err)
|
||||
return "", err
|
||||
}
|
||||
longPart := part
|
||||
if nametransform.IsLongContent(part) {
|
||||
longPart, err = nametransform.ReadLongNameAt(wd, part)
|
||||
if err != nil {
|
||||
fmt.Printf("ReadLongName: %v\n", err)
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
name, err := rn.nameTransform.DecryptName(longPart, dirIV)
|
||||
if err != nil {
|
||||
fmt.Printf("DecryptName: %v\n", err)
|
||||
return "", err
|
||||
}
|
||||
plainPath = path.Join(plainPath, name)
|
||||
|
@ -100,6 +108,5 @@ func (rn *RootNode) decryptPathAt(dirfd int, cipherPath string) (plainPath strin
|
|||
// and reliable.
|
||||
defer syscall.Close(wd)
|
||||
}
|
||||
|
||||
return plainPath, nil
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/rfjakob/gocryptfs/internal/nametransform"
|
||||
"github.com/rfjakob/gocryptfs/internal/tlog"
|
||||
)
|
||||
|
||||
|
@ -50,6 +49,9 @@ func (e *dirCacheEntry) Clear() {
|
|||
|
||||
type dirCache struct {
|
||||
sync.Mutex
|
||||
// Expected length of the stored IVs. Only used for sanity checks.
|
||||
// Usually set to 16, but 0 in plaintextnames mode.
|
||||
ivLen int
|
||||
// Cache entries
|
||||
entries [dirCacheSize]dirCacheEntry
|
||||
// Where to store the next entry (index into entries)
|
||||
|
@ -77,7 +79,7 @@ func (d *dirCache) Clear() {
|
|||
func (d *dirCache) Store(node *Node, fd int, iv []byte) {
|
||||
// Note: package ensurefds012, imported from main, guarantees that dirCache
|
||||
// can never get fds 0,1,2.
|
||||
if fd <= 0 || len(iv) != nametransform.DirIVLen {
|
||||
if fd <= 0 || len(iv) != d.ivLen {
|
||||
log.Panicf("Store sanity check failed: fd=%d len=%d", fd, len(iv))
|
||||
}
|
||||
d.Lock()
|
||||
|
@ -139,7 +141,7 @@ func (d *dirCache) Lookup(node *Node) (fd int, iv []byte) {
|
|||
if enableStats {
|
||||
d.hits++
|
||||
}
|
||||
if fd <= 0 || len(iv) != nametransform.DirIVLen {
|
||||
if fd <= 0 || len(iv) != d.ivLen {
|
||||
log.Panicf("Lookup sanity check failed: fd=%d len=%d", fd, len(iv))
|
||||
}
|
||||
d.dbg("dirCache.Lookup %p hit fd=%d dup=%d iv=%x\n", node, e.fd, fd, iv)
|
||||
|
|
|
@ -52,7 +52,7 @@ func (n *Node) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut)
|
|||
return f.(fs.FileGetattrer).Getattr(ctx, out)
|
||||
}
|
||||
|
||||
dirfd, cName, errno := n.prepareAtSyscall("")
|
||||
dirfd, cName, errno := n.prepareAtSyscallMyself()
|
||||
if errno != 0 {
|
||||
return
|
||||
}
|
||||
|
@ -106,7 +106,7 @@ func (n *Node) Unlink(ctx context.Context, name string) (errno syscall.Errno) {
|
|||
//
|
||||
// Symlink-safe through openBackingDir() + Readlinkat().
|
||||
func (n *Node) Readlink(ctx context.Context) (out []byte, errno syscall.Errno) {
|
||||
dirfd, cName, errno := n.prepareAtSyscall("")
|
||||
dirfd, cName, errno := n.prepareAtSyscallMyself()
|
||||
if errno != 0 {
|
||||
return
|
||||
}
|
||||
|
@ -123,7 +123,7 @@ func (n *Node) Setattr(ctx context.Context, f fs.FileHandle, in *fuse.SetAttrIn,
|
|||
return f2.Setattr(ctx, in, out)
|
||||
}
|
||||
|
||||
dirfd, cName, errno := n.prepareAtSyscall("")
|
||||
dirfd, cName, errno := n.prepareAtSyscallMyself()
|
||||
if errno != 0 {
|
||||
return
|
||||
}
|
||||
|
@ -271,7 +271,7 @@ func (n *Node) Link(ctx context.Context, target fs.InodeEmbedder, name string, o
|
|||
defer syscall.Close(dirfd)
|
||||
|
||||
n2 := toNode(target)
|
||||
dirfd2, cName2, errno := n2.prepareAtSyscall("")
|
||||
dirfd2, cName2, errno := n2.prepareAtSyscallMyself()
|
||||
if errno != 0 {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"syscall"
|
||||
|
||||
|
@ -154,7 +153,7 @@ func (n *Node) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.En
|
|||
// This function is symlink-safe through use of openBackingDir() and
|
||||
// ReadDirIVAt().
|
||||
func (n *Node) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) {
|
||||
parentDirFd, cDirName, errno := n.prepareAtSyscall("")
|
||||
parentDirFd, cDirName, errno := n.prepareAtSyscallMyself()
|
||||
if errno != 0 {
|
||||
return nil, errno
|
||||
}
|
||||
|
@ -239,15 +238,14 @@ func (n *Node) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) {
|
|||
// Symlink-safe through Unlinkat() + AT_REMOVEDIR.
|
||||
func (n *Node) Rmdir(ctx context.Context, name string) (code syscall.Errno) {
|
||||
rn := n.rootNode()
|
||||
p := filepath.Join(n.Path(), name)
|
||||
parentDirFd, cName, err := rn.openBackingDir(p)
|
||||
if err != nil {
|
||||
return fs.ToErrno(err)
|
||||
parentDirFd, cName, errno := n.prepareAtSyscall(name)
|
||||
if errno != 0 {
|
||||
return errno
|
||||
}
|
||||
defer syscall.Close(parentDirFd)
|
||||
if rn.args.PlaintextNames {
|
||||
// Unlinkat with AT_REMOVEDIR is equivalent to Rmdir
|
||||
err = unix.Unlinkat(parentDirFd, cName, unix.AT_REMOVEDIR)
|
||||
err := unix.Unlinkat(parentDirFd, cName, unix.AT_REMOVEDIR)
|
||||
return fs.ToErrno(err)
|
||||
}
|
||||
// Unless we are running as root, we need read, write and execute permissions
|
||||
|
@ -256,7 +254,7 @@ func (n *Node) Rmdir(ctx context.Context, name string) (code syscall.Errno) {
|
|||
var origMode uint32
|
||||
if !rn.args.PreserveOwner {
|
||||
var st unix.Stat_t
|
||||
err = syscallcompat.Fstatat(parentDirFd, cName, &st, unix.AT_SYMLINK_NOFOLLOW)
|
||||
err := syscallcompat.Fstatat(parentDirFd, cName, &st, unix.AT_SYMLINK_NOFOLLOW)
|
||||
if err != nil {
|
||||
return fs.ToErrno(err)
|
||||
}
|
||||
|
@ -360,7 +358,7 @@ retry:
|
|||
|
||||
// Opendir is a FUSE call to check if the directory can be opened.
|
||||
func (n *Node) Opendir(ctx context.Context) (errno syscall.Errno) {
|
||||
dirfd, cName, errno := n.prepareAtSyscall("")
|
||||
dirfd, cName, errno := n.prepareAtSyscallMyself()
|
||||
if errno != 0 {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -2,16 +2,12 @@ package fusefrontend
|
|||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
"github.com/hanwen/go-fuse/v2/fs"
|
||||
|
||||
"github.com/hanwen/go-fuse/v2/fuse"
|
||||
|
||||
"github.com/rfjakob/gocryptfs/internal/nametransform"
|
||||
"github.com/rfjakob/gocryptfs/internal/syscallcompat"
|
||||
"github.com/rfjakob/gocryptfs/internal/tlog"
|
||||
)
|
||||
|
@ -80,83 +76,6 @@ func (n *Node) rootNode() *RootNode {
|
|||
return n.Root().Operations().(*RootNode)
|
||||
}
|
||||
|
||||
// prepareAtSyscall returns a (dirfd, cName) pair that can be used
|
||||
// with the "___at" family of system calls (openat, fstatat, unlinkat...) to
|
||||
// access the backing encrypted directory.
|
||||
//
|
||||
// If you pass a `child` file name, the (dirfd, cName) pair will refer to
|
||||
// a child of this node.
|
||||
// If `child` is empty, the (dirfd, cName) pair refers to this node itself. For
|
||||
// the root node, that means (dirfd, ".").
|
||||
func (n *Node) prepareAtSyscall(child string) (dirfd int, cName string, errno syscall.Errno) {
|
||||
rn := n.rootNode()
|
||||
// all filesystem operations go through prepareAtSyscall(), so this is a
|
||||
// good place to reset the idle marker.
|
||||
atomic.StoreUint32(&rn.IsIdle, 0)
|
||||
|
||||
// root node itself is special
|
||||
if child == "" && n.IsRoot() {
|
||||
var err error
|
||||
dirfd, cName, err = rn.openBackingDir("")
|
||||
if err != nil {
|
||||
errno = fs.ToErrno(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// normal node itself can be converted to child of parent node
|
||||
if child == "" {
|
||||
name, p1 := n.Parent()
|
||||
if p1 == nil || name == "" {
|
||||
return -1, "", syscall.ENOENT
|
||||
}
|
||||
p2 := toNode(p1.Operations())
|
||||
return p2.prepareAtSyscall(name)
|
||||
}
|
||||
|
||||
// Cache lookup
|
||||
// TODO make it work for plaintextnames as well?
|
||||
cacheable := (!rn.args.PlaintextNames)
|
||||
if cacheable {
|
||||
var iv []byte
|
||||
dirfd, iv = rn.dirCache.Lookup(n)
|
||||
if dirfd > 0 {
|
||||
cName, err := rn.nameTransform.EncryptAndHashName(child, iv)
|
||||
if err != nil {
|
||||
return -1, "", fs.ToErrno(err)
|
||||
}
|
||||
return dirfd, cName, 0
|
||||
}
|
||||
}
|
||||
|
||||
// Slowpath
|
||||
if child == "" {
|
||||
log.Panicf("BUG: child name is empty - this cannot happen")
|
||||
}
|
||||
p := filepath.Join(n.Path(), child)
|
||||
if rn.isFiltered(p) {
|
||||
errno = syscall.EPERM
|
||||
return
|
||||
}
|
||||
dirfd, cName, err := rn.openBackingDir(p)
|
||||
if err != nil {
|
||||
errno = fs.ToErrno(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Cache store
|
||||
if cacheable {
|
||||
// TODO: openBackingDir already calls ReadDirIVAt(). Avoid duplicate work?
|
||||
iv, err := nametransform.ReadDirIVAt(dirfd)
|
||||
if err != nil {
|
||||
syscall.Close(dirfd)
|
||||
return -1, "", fs.ToErrno(err)
|
||||
}
|
||||
rn.dirCache.Store(n, dirfd, iv)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// newChild attaches a new child inode to n.
|
||||
// The passed-in `st` will be modified to get a unique inode number
|
||||
// (or, in `-sharedstorage` mode, the inode number will be set to zero).
|
||||
|
|
|
@ -16,7 +16,7 @@ import (
|
|||
//
|
||||
// Symlink-safe through Openat().
|
||||
func (n *Node) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
|
||||
dirfd, cName, errno := n.prepareAtSyscall("")
|
||||
dirfd, cName, errno := n.prepareAtSyscallMyself()
|
||||
if errno != 0 {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
package fusefrontend
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
"github.com/rfjakob/gocryptfs/internal/tlog"
|
||||
|
||||
"github.com/hanwen/go-fuse/v2/fs"
|
||||
|
||||
"github.com/rfjakob/gocryptfs/internal/nametransform"
|
||||
"github.com/rfjakob/gocryptfs/internal/syscallcompat"
|
||||
)
|
||||
|
||||
// prepareAtSyscall returns a (dirfd, cName) pair that can be used
|
||||
// with the "___at" family of system calls (openat, fstatat, unlinkat...) to
|
||||
// access the backing encrypted child file.
|
||||
func (n *Node) prepareAtSyscall(child string) (dirfd int, cName string, errno syscall.Errno) {
|
||||
if child == "" {
|
||||
tlog.Warn.Printf("BUG: prepareAtSyscall: child=%q, should have called prepareAtSyscallMyself", child)
|
||||
return n.prepareAtSyscallMyself()
|
||||
}
|
||||
|
||||
rn := n.rootNode()
|
||||
|
||||
// All filesystem operations go through here, so this is a good place
|
||||
// to reset the idle marker.
|
||||
atomic.StoreUint32(&rn.IsIdle, 0)
|
||||
|
||||
if n.IsRoot() && rn.isFiltered(child) {
|
||||
return -1, "", syscall.EPERM
|
||||
}
|
||||
|
||||
var encryptName func(int, string, []byte) (string, error)
|
||||
if !rn.args.PlaintextNames {
|
||||
encryptName = func(dirfd int, child string, iv []byte) (cName string, err error) {
|
||||
// Badname allowed, try to determine filenames
|
||||
if rn.nameTransform.HaveBadnamePatterns() {
|
||||
return rn.nameTransform.EncryptAndHashBadName(child, iv, dirfd)
|
||||
}
|
||||
return rn.nameTransform.EncryptAndHashName(child, iv)
|
||||
}
|
||||
}
|
||||
|
||||
// Cache lookup
|
||||
var iv []byte
|
||||
dirfd, iv = rn.dirCache.Lookup(n)
|
||||
if dirfd > 0 {
|
||||
if rn.args.PlaintextNames {
|
||||
return dirfd, child, 0
|
||||
}
|
||||
var err error
|
||||
cName, err = encryptName(dirfd, child, iv)
|
||||
if err != nil {
|
||||
syscall.Close(dirfd)
|
||||
return -1, "", fs.ToErrno(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Slowpath: Open ourselves & read diriv
|
||||
parentDirfd, myCName, errno := n.prepareAtSyscallMyself()
|
||||
if errno != 0 {
|
||||
return
|
||||
}
|
||||
defer syscall.Close(parentDirfd)
|
||||
|
||||
dirfd, err := syscallcompat.Openat(parentDirfd, myCName, syscall.O_NOFOLLOW|syscall.O_DIRECTORY|syscallcompat.O_PATH, 0)
|
||||
|
||||
// Cache store
|
||||
if !rn.args.PlaintextNames {
|
||||
var err error
|
||||
iv, err = nametransform.ReadDirIVAt(dirfd)
|
||||
if err != nil {
|
||||
syscall.Close(dirfd)
|
||||
return -1, "", fs.ToErrno(err)
|
||||
}
|
||||
}
|
||||
rn.dirCache.Store(n, dirfd, iv)
|
||||
|
||||
if rn.args.PlaintextNames {
|
||||
return dirfd, child, 0
|
||||
}
|
||||
|
||||
cName, err = encryptName(dirfd, child, iv)
|
||||
if err != nil {
|
||||
syscall.Close(dirfd)
|
||||
return -1, "", fs.ToErrno(err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (n *Node) prepareAtSyscallMyself() (dirfd int, cName string, errno syscall.Errno) {
|
||||
dirfd = -1
|
||||
|
||||
// Handle root node
|
||||
if n.IsRoot() {
|
||||
var err error
|
||||
rn := n.rootNode()
|
||||
// Open cipherdir (following symlinks)
|
||||
dirfd, err = syscallcompat.Open(rn.args.Cipherdir, syscall.O_DIRECTORY|syscallcompat.O_PATH, 0)
|
||||
if err != nil {
|
||||
return -1, "", fs.ToErrno(err)
|
||||
}
|
||||
return dirfd, ".", 0
|
||||
}
|
||||
|
||||
// Otherwise convert to prepareAtSyscall of parent node
|
||||
myName, p1 := n.Parent()
|
||||
if p1 == nil || myName == "" {
|
||||
errno = syscall.ENOENT
|
||||
return
|
||||
}
|
||||
parent := toNode(p1.Operations())
|
||||
return parent.prepareAtSyscall(myName)
|
||||
}
|
|
@ -20,7 +20,7 @@ func filterXattrSetFlags(flags int) int {
|
|||
}
|
||||
|
||||
func (n *Node) getXAttr(cAttr string) (out []byte, errno syscall.Errno) {
|
||||
dirfd, cName, errno := n.prepareAtSyscall("")
|
||||
dirfd, cName, errno := n.prepareAtSyscallMyself()
|
||||
if errno != 0 {
|
||||
return
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ func (n *Node) getXAttr(cAttr string) (out []byte, errno syscall.Errno) {
|
|||
}
|
||||
|
||||
func (n *Node) setXAttr(context *fuse.Context, cAttr string, cData []byte, flags uint32) (errno syscall.Errno) {
|
||||
dirfd, cName, errno := n.prepareAtSyscall("")
|
||||
dirfd, cName, errno := n.prepareAtSyscallMyself()
|
||||
if errno != 0 {
|
||||
return
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ func (n *Node) setXAttr(context *fuse.Context, cAttr string, cData []byte, flags
|
|||
}
|
||||
|
||||
func (n *Node) removeXAttr(cAttr string) (errno syscall.Errno) {
|
||||
dirfd, cName, errno := n.prepareAtSyscall("")
|
||||
dirfd, cName, errno := n.prepareAtSyscallMyself()
|
||||
if errno != 0 {
|
||||
return
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ func (n *Node) removeXAttr(cAttr string) (errno syscall.Errno) {
|
|||
}
|
||||
|
||||
func (n *Node) listXAttr() (out []string, errno syscall.Errno) {
|
||||
dirfd, cName, errno := n.prepareAtSyscall("")
|
||||
dirfd, cName, errno := n.prepareAtSyscallMyself()
|
||||
if errno != 0 {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ func filterXattrSetFlags(flags int) int {
|
|||
}
|
||||
|
||||
func (n *Node) getXAttr(cAttr string) (out []byte, errno syscall.Errno) {
|
||||
dirfd, cName, errno := n.prepareAtSyscall("")
|
||||
dirfd, cName, errno := n.prepareAtSyscallMyself()
|
||||
if errno != 0 {
|
||||
return
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ func (n *Node) getXAttr(cAttr string) (out []byte, errno syscall.Errno) {
|
|||
}
|
||||
|
||||
func (n *Node) setXAttr(context *fuse.Context, cAttr string, cData []byte, flags uint32) (errno syscall.Errno) {
|
||||
dirfd, cName, errno := n.prepareAtSyscall("")
|
||||
dirfd, cName, errno := n.prepareAtSyscallMyself()
|
||||
if errno != 0 {
|
||||
return
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ func (n *Node) setXAttr(context *fuse.Context, cAttr string, cData []byte, flags
|
|||
}
|
||||
|
||||
func (n *Node) removeXAttr(cAttr string) (errno syscall.Errno) {
|
||||
dirfd, cName, errno := n.prepareAtSyscall("")
|
||||
dirfd, cName, errno := n.prepareAtSyscallMyself()
|
||||
if errno != 0 {
|
||||
return
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ func (n *Node) removeXAttr(cAttr string) (errno syscall.Errno) {
|
|||
}
|
||||
|
||||
func (n *Node) listXAttr() (out []string, errno syscall.Errno) {
|
||||
dirfd, cName, errno := n.prepareAtSyscall("")
|
||||
dirfd, cName, errno := n.prepareAtSyscallMyself()
|
||||
if errno != 0 {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import (
|
|||
"github.com/rfjakob/gocryptfs/tests/test_helpers"
|
||||
)
|
||||
|
||||
func TestOpenBackingDir(t *testing.T) {
|
||||
func TestPrepareAtSyscall(t *testing.T) {
|
||||
cipherdir := test_helpers.InitFS(t)
|
||||
t.Logf("cipherdir = %q", cipherdir)
|
||||
args := Args{
|
||||
|
@ -33,9 +33,9 @@ func TestOpenBackingDir(t *testing.T) {
|
|||
t.Fatal(errno)
|
||||
}
|
||||
|
||||
dirfd, cName, err := rn.openBackingDir("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
dirfd, cName, errno := rn.prepareAtSyscallMyself()
|
||||
if errno != 0 {
|
||||
t.Fatal(errno)
|
||||
}
|
||||
if cName != "." {
|
||||
t.Fatal("cName should be .")
|
||||
|
@ -44,15 +44,15 @@ func TestOpenBackingDir(t *testing.T) {
|
|||
|
||||
// Again, but populate the cache for "" by looking up a non-existing file
|
||||
rn.Lookup(nil, "xyz1234", &fuse.EntryOut{})
|
||||
dirfd, cName, err = rn.openBackingDir("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
dirfd, cName, errno = rn.prepareAtSyscallMyself()
|
||||
if errno != 0 {
|
||||
t.Fatal(errno)
|
||||
}
|
||||
if cName != "." {
|
||||
t.Fatal("cName should be .")
|
||||
}
|
||||
|
||||
err = syscallcompat.Faccessat(dirfd, cName, unix.R_OK)
|
||||
err := syscallcompat.Faccessat(dirfd, cName, unix.R_OK)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ func TestOpenBackingDir(t *testing.T) {
|
|||
}
|
||||
syscall.Close(dirfd)
|
||||
|
||||
dirfd, cName, err = rn.openBackingDir("dir1")
|
||||
dirfd, cName, errno = rn.prepareAtSyscall("dir1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -75,9 +75,9 @@ func TestOpenBackingDir(t *testing.T) {
|
|||
}
|
||||
syscall.Close(dirfd)
|
||||
|
||||
dirfd, cName, err = rn.openBackingDir("dir1/dir2")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
dirfd, cName, errno = dir1.prepareAtSyscall("dir2")
|
||||
if errno != 0 {
|
||||
t.Fatal(errno)
|
||||
}
|
||||
if cName == "" {
|
||||
t.Fatal("cName should not be empty")
|
||||
|
@ -90,9 +90,9 @@ func TestOpenBackingDir(t *testing.T) {
|
|||
|
||||
n255 := strings.Repeat("n", 255)
|
||||
dir1.Mkdir(nil, n255, 0700, out)
|
||||
dirfd, cName, err = rn.openBackingDir("dir1/" + n255)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
dirfd, cName, errno = dir1.prepareAtSyscall(n255)
|
||||
if errno != 0 {
|
||||
t.Fatal(errno)
|
||||
}
|
||||
if cName == "" {
|
||||
t.Fatal("cName should not be empty")
|
||||
|
@ -107,32 +107,34 @@ func TestOpenBackingDir(t *testing.T) {
|
|||
syscall.Close(dirfd)
|
||||
}
|
||||
|
||||
func TestOpenBackingDirPlaintextNames(t *testing.T) {
|
||||
func TestPrepareAtSyscallPlaintextnames(t *testing.T) {
|
||||
cipherdir := test_helpers.InitFS(t, "-plaintextnames")
|
||||
args := Args{
|
||||
Cipherdir: cipherdir,
|
||||
PlaintextNames: true,
|
||||
}
|
||||
fs := newTestFS(args)
|
||||
rn := newTestFS(args)
|
||||
out := &fuse.EntryOut{}
|
||||
|
||||
_, errno := fs.Mkdir(nil, "dir1", 0700, out)
|
||||
child, errno := rn.Mkdir(nil, "dir1", 0700, out)
|
||||
if errno != 0 {
|
||||
t.Fatal(errno)
|
||||
}
|
||||
_, errno = fs.Mkdir(nil, "dir1/dir2", 0700, out)
|
||||
rn.AddChild("dir1", child, false)
|
||||
dir1 := toNode(child.Operations())
|
||||
_, errno = dir1.Mkdir(nil, "dir2", 0700, out)
|
||||
if errno != 0 {
|
||||
t.Fatal(errno)
|
||||
}
|
||||
|
||||
dirfd, cName, err := fs.openBackingDir("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
dirfd, cName, errno := rn.prepareAtSyscallMyself()
|
||||
if errno != 0 {
|
||||
t.Fatal(errno)
|
||||
}
|
||||
if cName != "." {
|
||||
t.Fatal("cName should be .")
|
||||
}
|
||||
err = syscallcompat.Faccessat(dirfd, cName, unix.R_OK)
|
||||
err := syscallcompat.Faccessat(dirfd, cName, unix.R_OK)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
@ -142,9 +144,9 @@ func TestOpenBackingDirPlaintextNames(t *testing.T) {
|
|||
}
|
||||
syscall.Close(dirfd)
|
||||
|
||||
dirfd, cName, err = fs.openBackingDir("dir1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
dirfd, cName, errno = rn.prepareAtSyscall("dir1")
|
||||
if errno != 0 {
|
||||
t.Fatal(errno)
|
||||
}
|
||||
if cName != "dir1" {
|
||||
t.Fatalf("wrong cName: %q", cName)
|
||||
|
@ -155,9 +157,9 @@ func TestOpenBackingDirPlaintextNames(t *testing.T) {
|
|||
}
|
||||
syscall.Close(dirfd)
|
||||
|
||||
dirfd, cName, err = fs.openBackingDir("dir1/dir2")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
dirfd, cName, errno = dir1.prepareAtSyscall("dir2")
|
||||
if errno != 0 {
|
||||
t.Fatal(errno)
|
||||
}
|
||||
if cName != "dir2" {
|
||||
t.Fatalf("wrong cName: %q", cName)
|
|
@ -2,7 +2,6 @@ package fusefrontend
|
|||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
@ -27,7 +26,7 @@ type RootNode struct {
|
|||
// states
|
||||
dirIVLock sync.RWMutex
|
||||
// Filename encryption helper
|
||||
nameTransform nametransform.NameTransformer
|
||||
nameTransform *nametransform.NameTransform
|
||||
// Content encryption helper
|
||||
contentEnc *contentenc.ContentEnc
|
||||
// This lock is used by openWriteOnlyFile() to block concurrent opens while
|
||||
|
@ -54,18 +53,23 @@ type RootNode struct {
|
|||
inoMap inomap.TranslateStater
|
||||
}
|
||||
|
||||
func NewRootNode(args Args, c *contentenc.ContentEnc, n nametransform.NameTransformer) *RootNode {
|
||||
func NewRootNode(args Args, c *contentenc.ContentEnc, n *nametransform.NameTransform) *RootNode {
|
||||
if args.SerializeReads {
|
||||
serialize_reads.InitSerializer()
|
||||
}
|
||||
if len(args.Exclude) > 0 {
|
||||
tlog.Warn.Printf("Forward mode does not support -exclude")
|
||||
}
|
||||
ivLen := nametransform.DirIVLen
|
||||
if args.PlaintextNames {
|
||||
ivLen = 0
|
||||
}
|
||||
rn := &RootNode{
|
||||
args: args,
|
||||
nameTransform: n,
|
||||
contentEnc: c,
|
||||
inoMap: inomap.New(),
|
||||
dirCache: dirCache{ivLen: ivLen},
|
||||
}
|
||||
// In `-sharedstorage` mode we always set the inode number to zero.
|
||||
// This makes go-fuse generate a new inode number for each lookup.
|
||||
|
@ -122,15 +126,15 @@ func (rn *RootNode) reportMitigatedCorruption(item string) {
|
|||
}
|
||||
}
|
||||
|
||||
// isFiltered - check if plaintext "path" should be forbidden
|
||||
// isFiltered - check if plaintext file "child" should be forbidden
|
||||
//
|
||||
// Prevents name clashes with internal files when file names are not encrypted
|
||||
func (rn *RootNode) isFiltered(path string) bool {
|
||||
func (rn *RootNode) isFiltered(child string) bool {
|
||||
if !rn.args.PlaintextNames {
|
||||
return false
|
||||
}
|
||||
// gocryptfs.conf in the root directory is forbidden
|
||||
if path == configfile.ConfDefaultName {
|
||||
if child == configfile.ConfDefaultName {
|
||||
tlog.Info.Printf("The name /%s is reserved when -plaintextnames is used\n",
|
||||
configfile.ConfDefaultName)
|
||||
return true
|
||||
|
@ -205,66 +209,6 @@ func (rn *RootNode) openWriteOnlyFile(dirfd int, cName string, newFlags int) (rw
|
|||
return syscallcompat.Openat(dirfd, cName, newFlags, 0)
|
||||
}
|
||||
|
||||
// openBackingDir opens the parent ciphertext directory of plaintext path
|
||||
// "relPath". It returns the dirfd (opened with O_PATH) and the encrypted
|
||||
// basename.
|
||||
//
|
||||
// The caller should then use Openat(dirfd, cName, ...) and friends.
|
||||
// For convenience, if relPath is "", cName is going to be ".".
|
||||
//
|
||||
// openBackingDir is secure against symlink races by using Openat and
|
||||
// ReadDirIVAt.
|
||||
//
|
||||
// Retries on EINTR.
|
||||
func (rn *RootNode) openBackingDir(relPath string) (dirfd int, cName string, err error) {
|
||||
dirRelPath := nametransform.Dir(relPath)
|
||||
// With PlaintextNames, we don't need to read DirIVs. Easy.
|
||||
if rn.args.PlaintextNames {
|
||||
dirfd, err = syscallcompat.OpenDirNofollow(rn.args.Cipherdir, dirRelPath)
|
||||
if err != nil {
|
||||
return -1, "", err
|
||||
}
|
||||
// If relPath is empty, cName is ".".
|
||||
cName = filepath.Base(relPath)
|
||||
return dirfd, cName, nil
|
||||
}
|
||||
// Open cipherdir (following symlinks)
|
||||
dirfd, err = syscallcompat.Open(rn.args.Cipherdir, syscall.O_DIRECTORY|syscallcompat.O_PATH, 0)
|
||||
if err != nil {
|
||||
return -1, "", err
|
||||
}
|
||||
// If relPath is empty, cName is ".".
|
||||
if relPath == "" {
|
||||
return dirfd, ".", nil
|
||||
}
|
||||
// Walk the directory tree
|
||||
parts := strings.Split(relPath, "/")
|
||||
for i, name := range parts {
|
||||
iv, err := nametransform.ReadDirIVAt(dirfd)
|
||||
if err != nil {
|
||||
syscall.Close(dirfd)
|
||||
return -1, "", err
|
||||
}
|
||||
cName, err = rn.nameTransform.EncryptAndHashName(name, iv)
|
||||
if err != nil {
|
||||
syscall.Close(dirfd)
|
||||
return -1, "", err
|
||||
}
|
||||
// Last part? We are done.
|
||||
if i == len(parts)-1 {
|
||||
break
|
||||
}
|
||||
// Not the last part? Descend into next directory.
|
||||
dirfd2, err := syscallcompat.Openat(dirfd, cName, syscall.O_NOFOLLOW|syscall.O_DIRECTORY|syscallcompat.O_PATH, 0)
|
||||
syscall.Close(dirfd)
|
||||
if err != nil {
|
||||
return -1, "", err
|
||||
}
|
||||
dirfd = dirfd2
|
||||
}
|
||||
return dirfd, cName, nil
|
||||
}
|
||||
|
||||
// encryptSymlinkTarget: "data" is encrypted like file contents (GCM)
|
||||
// and base64-encoded.
|
||||
// The empty string encrypts to the empty string.
|
||||
|
|
|
@ -19,7 +19,7 @@ func newTestFS(args Args) *RootNode {
|
|||
key := make([]byte, cryptocore.KeyLen)
|
||||
cCore := cryptocore.New(key, cryptocore.BackendGoGCM, contentenc.DefaultIVBits, true, false)
|
||||
cEnc := contentenc.New(cCore, contentenc.DefaultBS, false)
|
||||
n := nametransform.New(cCore.EMECipher, true, true)
|
||||
n := nametransform.New(cCore.EMECipher, true, true, nil)
|
||||
rn := NewRootNode(args, cEnc, n)
|
||||
oneSec := time.Second
|
||||
options := &fs.Options{
|
||||
|
|
|
@ -64,12 +64,3 @@ func TestShouldReturnFalseIfThereAreNoExclusions(t *testing.T) {
|
|||
t.Error("Should not exclude any path if no exclusions were specified")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldCallIgnoreParserToCheckExclusion(t *testing.T) {
|
||||
rfs, ignorerMock := createRFSWithMocks()
|
||||
|
||||
rfs.isExcludedPlain("some/path")
|
||||
if ignorerMock.calledWith != "some/path" {
|
||||
t.Error("Failed to call IgnoreParser")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
package fusefrontend_reverse
|
||||
|
||||
import (
|
||||
"github.com/rfjakob/gocryptfs/internal/nametransform"
|
||||
)
|
||||
|
||||
type IgnoreParserMock struct {
|
||||
toExclude string
|
||||
calledWith string
|
||||
}
|
||||
|
||||
func (parser *IgnoreParserMock) MatchesPath(f string) bool {
|
||||
parser.calledWith = f
|
||||
return f == parser.toExclude
|
||||
}
|
||||
|
||||
type NameTransformMock struct {
|
||||
nametransform.NameTransform
|
||||
}
|
||||
|
||||
func (n *NameTransformMock) DecryptName(cipherName string, iv []byte) (string, error) {
|
||||
return "mockdecrypt_" + cipherName, nil
|
||||
}
|
||||
|
||||
func createRFSWithMocks() (*RootNode, *IgnoreParserMock) {
|
||||
ignorerMock := &IgnoreParserMock{}
|
||||
nameTransformMock := &NameTransformMock{}
|
||||
var rfs RootNode
|
||||
rfs.excluder = ignorerMock
|
||||
rfs.nameTransform = nameTransformMock
|
||||
return &rfs, ignorerMock
|
||||
}
|
|
@ -28,7 +28,7 @@ type RootNode struct {
|
|||
// Stores configuration arguments
|
||||
args fusefrontend.Args
|
||||
// Filename encryption helper
|
||||
nameTransform nametransform.NameTransformer
|
||||
nameTransform *nametransform.NameTransform
|
||||
// Content encryption helper
|
||||
contentEnc *contentenc.ContentEnc
|
||||
// Tests whether a path is excluded (hidden) from the user. Used by -exclude.
|
||||
|
@ -41,7 +41,7 @@ type RootNode struct {
|
|||
// NewRootNode returns an encrypted FUSE overlay filesystem.
|
||||
// In this case (reverse mode) the backing directory is plain-text and
|
||||
// ReverseFS provides an encrypted view.
|
||||
func NewRootNode(args fusefrontend.Args, c *contentenc.ContentEnc, n nametransform.NameTransformer) *RootNode {
|
||||
func NewRootNode(args fusefrontend.Args, c *contentenc.ContentEnc, n *nametransform.NameTransform) *RootNode {
|
||||
rn := &RootNode{
|
||||
args: args,
|
||||
nameTransform: n,
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
package nametransform
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/rfjakob/gocryptfs/internal/syscallcompat"
|
||||
)
|
||||
|
||||
const (
|
||||
// BadnameSuffix is appended to filenames in plaintext view if a corrupt
|
||||
// ciphername is shown due to a matching `-badname` pattern
|
||||
BadnameSuffix = " GOCRYPTFS_BAD_NAME"
|
||||
)
|
||||
|
||||
// EncryptAndHashBadName tries to find the "name" substring, which (encrypted and hashed)
|
||||
// leads to an unique existing file
|
||||
// Returns ENOENT if cipher file does not exist or is not unique
|
||||
func (be *NameTransform) EncryptAndHashBadName(name string, iv []byte, dirfd int) (cName string, err error) {
|
||||
var st unix.Stat_t
|
||||
var filesFound int
|
||||
lastFoundName, err := be.EncryptAndHashName(name, iv)
|
||||
if !strings.HasSuffix(name, BadnameSuffix) || err != nil {
|
||||
//Default mode: same behaviour on error or no BadNameFlag on "name"
|
||||
return lastFoundName, err
|
||||
}
|
||||
//Default mode: Check if File extists without modifications
|
||||
err = syscallcompat.Fstatat(dirfd, lastFoundName, &st, unix.AT_SYMLINK_NOFOLLOW)
|
||||
if err == nil {
|
||||
//file found, return result
|
||||
return lastFoundName, nil
|
||||
}
|
||||
//BadName Mode: check if the name was tranformed without change (badname suffix and undecryptable cipher name)
|
||||
err = syscallcompat.Fstatat(dirfd, name[:len(name)-len(BadnameSuffix)], &st, unix.AT_SYMLINK_NOFOLLOW)
|
||||
if err == nil {
|
||||
filesFound++
|
||||
lastFoundName = name[:len(name)-len(BadnameSuffix)]
|
||||
}
|
||||
// search for the longest badname pattern match
|
||||
for charpos := len(name) - len(BadnameSuffix); charpos > 0; charpos-- {
|
||||
//only use original cipher name and append assumed suffix (without badname flag)
|
||||
cNamePart, err := be.EncryptName(name[:charpos], iv)
|
||||
if err != nil {
|
||||
//expand suffix on error
|
||||
continue
|
||||
}
|
||||
if be.longNames && len(cName) > NameMax {
|
||||
cNamePart = be.HashLongName(cName)
|
||||
}
|
||||
cNameBadReverse := cNamePart + name[charpos:len(name)-len(BadnameSuffix)]
|
||||
err = syscallcompat.Fstatat(dirfd, cNameBadReverse, &st, unix.AT_SYMLINK_NOFOLLOW)
|
||||
if err == nil {
|
||||
filesFound++
|
||||
lastFoundName = cNameBadReverse
|
||||
}
|
||||
}
|
||||
if filesFound == 1 {
|
||||
return lastFoundName, nil
|
||||
}
|
||||
// more than 1 possible file found, ignore
|
||||
return "", syscall.ENOENT
|
||||
}
|
||||
|
||||
func (n *NameTransform) decryptBadname(cipherName string, iv []byte) (string, error) {
|
||||
for _, pattern := range n.badnamePatterns {
|
||||
match, err := filepath.Match(pattern, cipherName)
|
||||
// Pattern should have been validated already
|
||||
if err == nil && match {
|
||||
// Find longest decryptable substring
|
||||
// At least 16 bytes due to AES --> at least 22 characters in base64
|
||||
nameMin := n.B64.EncodedLen(aes.BlockSize)
|
||||
for charpos := len(cipherName) - 1; charpos >= nameMin; charpos-- {
|
||||
res, err := n.decryptName(cipherName[:charpos], iv)
|
||||
if err == nil {
|
||||
return res + cipherName[charpos:] + BadnameSuffix, nil
|
||||
}
|
||||
}
|
||||
return cipherName + BadnameSuffix, nil
|
||||
}
|
||||
}
|
||||
return "", syscall.EBADMSG
|
||||
}
|
||||
|
||||
// HaveBadnamePatterns returns true if `-badname` patterns were provided
|
||||
func (n *NameTransform) HaveBadnamePatterns() bool {
|
||||
return len(n.badnamePatterns) > 0
|
||||
}
|
|
@ -5,7 +5,6 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/rfjakob/gocryptfs/internal/cryptocore"
|
||||
|
@ -93,30 +92,3 @@ func WriteDirIVAt(dirfd int) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// encryptAndHashName encrypts "name" and hashes it to a longname if it is
|
||||
// too long.
|
||||
// Returns ENAMETOOLONG if "name" is longer than 255 bytes.
|
||||
func (be *NameTransform) EncryptAndHashName(name string, iv []byte) (string, error) {
|
||||
// Prevent the user from creating files longer than 255 chars.
|
||||
if len(name) > NameMax {
|
||||
return "", syscall.ENAMETOOLONG
|
||||
}
|
||||
cName, err := be.EncryptName(name, iv)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if be.longNames && len(cName) > NameMax {
|
||||
return be.HashLongName(cName), nil
|
||||
}
|
||||
return cName, nil
|
||||
}
|
||||
|
||||
// Dir is like filepath.Dir but returns "" instead of ".".
|
||||
func Dir(path string) string {
|
||||
d := filepath.Dir(path)
|
||||
if d == "." {
|
||||
return ""
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
|
|
@ -17,21 +17,6 @@ const (
|
|||
NameMax = 255
|
||||
)
|
||||
|
||||
// NameTransformer is an interface used to transform filenames.
|
||||
type NameTransformer interface {
|
||||
DecryptName(cipherName string, iv []byte) (string, error)
|
||||
EncryptName(plainName string, iv []byte) (string, error)
|
||||
EncryptAndHashName(name string, iv []byte) (string, error)
|
||||
// HashLongName - take the hash of a long string "name" and return
|
||||
// "gocryptfs.longname.[sha256]"
|
||||
//
|
||||
// This function does not do any I/O.
|
||||
HashLongName(name string) string
|
||||
WriteLongNameAt(dirfd int, hashName string, plainName string) error
|
||||
B64EncodeToString(src []byte) string
|
||||
B64DecodeString(s string) ([]byte, error)
|
||||
}
|
||||
|
||||
// NameTransform is used to transform filenames.
|
||||
type NameTransform struct {
|
||||
emeCipher *eme.EMECipher
|
||||
|
@ -40,19 +25,23 @@ type NameTransform struct {
|
|||
// on the Raw64 feature flag
|
||||
B64 *base64.Encoding
|
||||
// Patterns to bypass decryption
|
||||
BadnamePatterns []string
|
||||
badnamePatterns []string
|
||||
}
|
||||
|
||||
// New returns a new NameTransform instance.
|
||||
func New(e *eme.EMECipher, longNames bool, raw64 bool) *NameTransform {
|
||||
func New(e *eme.EMECipher, longNames bool, raw64 bool, badname []string) *NameTransform {
|
||||
tlog.Debug.Printf("nametransform.New: longNames=%v, raw64=%v, badname=%q",
|
||||
longNames, raw64, badname)
|
||||
|
||||
b64 := base64.URLEncoding
|
||||
if raw64 {
|
||||
b64 = base64.RawURLEncoding
|
||||
}
|
||||
return &NameTransform{
|
||||
emeCipher: e,
|
||||
longNames: longNames,
|
||||
B64: b64,
|
||||
emeCipher: e,
|
||||
longNames: longNames,
|
||||
B64: b64,
|
||||
badnamePatterns: badname,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,22 +49,8 @@ func New(e *eme.EMECipher, longNames bool, raw64 bool) *NameTransform {
|
|||
// filename "cipherName", and failing that checks if it can be bypassed
|
||||
func (n *NameTransform) DecryptName(cipherName string, iv []byte) (string, error) {
|
||||
res, err := n.decryptName(cipherName, iv)
|
||||
if err != nil {
|
||||
for _, pattern := range n.BadnamePatterns {
|
||||
match, err := filepath.Match(pattern, cipherName)
|
||||
if err == nil && match { // Pattern should have been validated already
|
||||
// Find longest decryptable substring
|
||||
// At least 16 bytes due to AES --> at least 22 characters in base64
|
||||
nameMin := n.B64.EncodedLen(aes.BlockSize)
|
||||
for charpos := len(cipherName) - 1; charpos >= nameMin; charpos-- {
|
||||
res, err = n.decryptName(cipherName[:charpos], iv)
|
||||
if err == nil {
|
||||
return res + cipherName[charpos:] + " GOCRYPTFS_BAD_NAME", nil
|
||||
}
|
||||
}
|
||||
return cipherName + " GOCRYPTFS_BAD_NAME", nil
|
||||
}
|
||||
}
|
||||
if err != nil && n.HaveBadnamePatterns() {
|
||||
return n.decryptBadname(cipherName, iv)
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
@ -126,6 +101,24 @@ func (n *NameTransform) EncryptName(plainName string, iv []byte) (cipherName64 s
|
|||
return cipherName64, nil
|
||||
}
|
||||
|
||||
// EncryptAndHashName encrypts "name" and hashes it to a longname if it is
|
||||
// too long.
|
||||
// Returns ENAMETOOLONG if "name" is longer than 255 bytes.
|
||||
func (be *NameTransform) EncryptAndHashName(name string, iv []byte) (string, error) {
|
||||
// Prevent the user from creating files longer than 255 chars.
|
||||
if len(name) > NameMax {
|
||||
return "", syscall.ENAMETOOLONG
|
||||
}
|
||||
cName, err := be.EncryptName(name, iv)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if be.longNames && len(cName) > NameMax {
|
||||
return be.HashLongName(cName), nil
|
||||
}
|
||||
return cName, nil
|
||||
}
|
||||
|
||||
// B64EncodeToString returns a Base64-encoded string
|
||||
func (n *NameTransform) B64EncodeToString(src []byte) string {
|
||||
return n.B64.EncodeToString(src)
|
||||
|
@ -135,3 +128,12 @@ func (n *NameTransform) B64EncodeToString(src []byte) string {
|
|||
func (n *NameTransform) B64DecodeString(s string) ([]byte, error) {
|
||||
return n.B64.DecodeString(s)
|
||||
}
|
||||
|
||||
// Dir is like filepath.Dir but returns "" instead of ".".
|
||||
func Dir(path string) string {
|
||||
d := filepath.Dir(path)
|
||||
if d == "." {
|
||||
return ""
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
|
7
main.go
7
main.go
|
@ -176,6 +176,7 @@ func main() {
|
|||
if args.debug {
|
||||
tlog.Debug.Enabled = true
|
||||
}
|
||||
tlog.Debug.Printf("cli args: %q", os.Args)
|
||||
// "-v"
|
||||
if args.version {
|
||||
tlog.Debug.Printf("openssl=%v\n", args.openssl)
|
||||
|
@ -282,12 +283,6 @@ func main() {
|
|||
if args.cpuprofile != "" || args.memprofile != "" || args.trace != "" {
|
||||
tlog.Info.Printf("Note: You must unmount gracefully, otherwise the profile file(s) will stay empty!\n")
|
||||
}
|
||||
// "-openssl"
|
||||
if !args.openssl {
|
||||
tlog.Debug.Printf("OpenSSL disabled, using Go GCM")
|
||||
} else {
|
||||
tlog.Debug.Printf("OpenSSL enabled")
|
||||
}
|
||||
// Operation flags
|
||||
nOps := countOpFlags(&args)
|
||||
if nOps == 0 {
|
||||
|
|
19
mount.go
19
mount.go
|
@ -2,7 +2,6 @@ package main
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/syslog"
|
||||
|
@ -118,8 +117,6 @@ func doMount(args *argContainer) {
|
|||
args.noprealloc = true
|
||||
}
|
||||
}
|
||||
// We cannot use JSON for pretty-printing as the fields are unexported
|
||||
tlog.Debug.Printf("cli args: %#v", args)
|
||||
// Initialize gocryptfs (read config file, ask for password, ...)
|
||||
fs, wipeKeys := initFuseFrontend(args)
|
||||
// Try to wipe secret keys from memory after unmount
|
||||
|
@ -309,24 +306,11 @@ func initFuseFrontend(args *argContainer) (rootNode fs.InodeEmbedder, wipeKeys f
|
|||
if args.allow_other && os.Getuid() == 0 {
|
||||
frontendArgs.PreserveOwner = true
|
||||
}
|
||||
jsonBytes, _ := json.MarshalIndent(frontendArgs, "", "\t")
|
||||
tlog.Debug.Printf("frontendArgs: %s", string(jsonBytes))
|
||||
|
||||
// Init crypto backend
|
||||
cCore := cryptocore.New(masterkey, cryptoBackend, contentenc.DefaultIVBits, args.hkdf, args.forcedecode)
|
||||
cEnc := contentenc.New(cCore, contentenc.DefaultBS, args.forcedecode)
|
||||
nameTransform := nametransform.New(cCore.EMECipher, frontendArgs.LongNames, args.raw64)
|
||||
// Init badname patterns
|
||||
nameTransform.BadnamePatterns = make([]string, 0)
|
||||
for _, pattern := range args.badname {
|
||||
_, err := filepath.Match(pattern, "") // Make sure pattern is valid
|
||||
if err != nil {
|
||||
tlog.Fatal.Printf("-badname: invalid pattern %q supplied", pattern)
|
||||
os.Exit(exitcodes.Usage)
|
||||
} else {
|
||||
nameTransform.BadnamePatterns = append(nameTransform.BadnamePatterns, pattern)
|
||||
}
|
||||
}
|
||||
nameTransform := nametransform.New(cCore.EMECipher, frontendArgs.LongNames, args.raw64, []string(args.badname))
|
||||
// After the crypto backend is initialized,
|
||||
// we can purge the master key from memory.
|
||||
for i := range masterkey {
|
||||
|
@ -334,6 +318,7 @@ func initFuseFrontend(args *argContainer) (rootNode fs.InodeEmbedder, wipeKeys f
|
|||
}
|
||||
masterkey = nil
|
||||
// Spawn fusefrontend
|
||||
tlog.Debug.Printf("frontendArgs: %s", tlog.JSONDump(frontendArgs))
|
||||
if args.reverse {
|
||||
if cryptoBackend != cryptocore.BackendAESSIV {
|
||||
log.Panic("reverse mode must use AES-SIV, everything else is insecure")
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
|
||||
"github.com/rfjakob/gocryptfs/internal/configfile"
|
||||
"github.com/rfjakob/gocryptfs/internal/exitcodes"
|
||||
"github.com/rfjakob/gocryptfs/internal/nametransform"
|
||||
|
||||
"github.com/rfjakob/gocryptfs/tests/test_helpers"
|
||||
)
|
||||
|
@ -698,18 +699,29 @@ func TestSymlinkedCipherdir(t *testing.T) {
|
|||
|
||||
// TestBadname tests the `-badname` option
|
||||
func TestBadname(t *testing.T) {
|
||||
//Supported structure of badname: <ciphername><badname pattern><badname suffix>
|
||||
//"Visible" shows the success of function DecryptName (cipher -> plain)
|
||||
//"Access" shows the success of function EncryptAndHashBadName (plain -> cipher)
|
||||
//Case Visible Access Description
|
||||
//Case 1 x x Access file without BadName suffix (default mode)
|
||||
//Case 2 x x Access file with BadName suffix which has a valid cipher file (will only be possible if file was created without badname option)
|
||||
//Case 3 Access file with valid ciphername + BadName suffix (impossible since this would not be produced by DecryptName)
|
||||
//Case 4 x x Access file with decryptable part of name and Badname suffix (default badname case)
|
||||
//Case 5 x x Access file with undecryptable name and BadName suffix (e. g. when part of the cipher name was cut)
|
||||
//Case 6 x Access file with multiple possible matches.
|
||||
//Case 7 Access file with BadName suffix and non-matching pattern
|
||||
|
||||
dir := test_helpers.InitFS(t)
|
||||
mnt := dir + ".mnt"
|
||||
validFileName := "file"
|
||||
invalidSuffix := ".invalid_file"
|
||||
invalidSuffix := "_invalid_file"
|
||||
var contentCipher [7][]byte
|
||||
//first mount without badname (see case 2)
|
||||
test_helpers.MountOrFatal(t, dir, mnt, "-extpass=echo test", "-wpanic=false")
|
||||
|
||||
// use static suffix for testing
|
||||
test_helpers.MountOrFatal(t, dir, mnt, "-badname=*", "-extpass=echo test")
|
||||
defer test_helpers.UnmountPanic(mnt)
|
||||
|
||||
// write one valid filename (empty content)
|
||||
file := mnt + "/" + validFileName
|
||||
err := ioutil.WriteFile(file, nil, 0600)
|
||||
// Case 1: write one valid filename (empty content)
|
||||
err := ioutil.WriteFile(file, []byte("Content Case 1."), 0600)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -720,7 +732,6 @@ func TestBadname(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
defer fread.Close()
|
||||
|
||||
encryptedfilename := ""
|
||||
ciphernames, err := fread.Readdirnames(0)
|
||||
if err != nil {
|
||||
|
@ -733,14 +744,64 @@ func TestBadname(t *testing.T) {
|
|||
break
|
||||
}
|
||||
}
|
||||
//Generate valid cipherdata for all cases
|
||||
for i := 0; i < len(contentCipher); i++ {
|
||||
err := ioutil.WriteFile(file, []byte(fmt.Sprintf("Content Case %d.", i+1)), 0600)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
//save the cipher data for file operations in cipher dir
|
||||
contentCipher[i], err = ioutil.ReadFile(dir + "/" + encryptedfilename)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// write invalid file which should be decodable
|
||||
err = ioutil.WriteFile(dir+"/"+encryptedfilename+invalidSuffix, nil, 0600)
|
||||
//re-write content for case 1
|
||||
err = ioutil.WriteFile(file, []byte("Content Case 1."), 0600)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// write invalid file which is not decodable (cropping the encrpyted file name)
|
||||
err = ioutil.WriteFile(dir+"/"+encryptedfilename[:len(encryptedfilename)-2]+invalidSuffix, nil, 0600)
|
||||
|
||||
// Case 2: File with invalid suffix in plain name but valid cipher file
|
||||
file = mnt + "/" + validFileName + nametransform.BadnameSuffix
|
||||
err = ioutil.WriteFile(file, []byte("Content Case 2."), 0600)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// unmount...
|
||||
test_helpers.UnmountPanic(mnt)
|
||||
|
||||
// ...and remount with -badname.
|
||||
test_helpers.MountOrFatal(t, dir, mnt, "-badname=*valid*", "-extpass=echo test", "-wpanic=false")
|
||||
defer test_helpers.UnmountPanic(mnt)
|
||||
|
||||
// Case 3 is impossible: only BadnameSuffix would mean the cipher name is valid
|
||||
|
||||
// Case 4: write invalid file which should be decodable
|
||||
err = ioutil.WriteFile(dir+"/"+encryptedfilename+invalidSuffix, contentCipher[3], 0600)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
//Case 5: write invalid file which is not decodable (cropping the encrpyted file name)
|
||||
err = ioutil.WriteFile(dir+"/"+encryptedfilename[:len(encryptedfilename)-2]+invalidSuffix, contentCipher[4], 0600)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Case 6: Multiple possible matches
|
||||
// generate two files with invalid cipher names which can both match the badname pattern
|
||||
err = ioutil.WriteFile(dir+"/mzaZRF9_0IU-_5vv2wPC"+invalidSuffix, contentCipher[5], 0600)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = ioutil.WriteFile(dir+"/mzaZRF9_0IU-_5vv2wP"+invalidSuffix, contentCipher[5], 0600)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Case 7: Non-Matching badname pattern
|
||||
err = ioutil.WriteFile(dir+"/"+encryptedfilename+"wrongPattern", contentCipher[6], 0600)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -755,22 +816,74 @@ func TestBadname(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
foundDecodable := false
|
||||
foundUndecodable := false
|
||||
|
||||
searchstrings := []string{
|
||||
validFileName,
|
||||
validFileName + nametransform.BadnameSuffix,
|
||||
"",
|
||||
validFileName + invalidSuffix + nametransform.BadnameSuffix,
|
||||
encryptedfilename[:len(encryptedfilename)-2] + invalidSuffix + nametransform.BadnameSuffix,
|
||||
"",
|
||||
validFileName + "wrongPattern" + nametransform.BadnameSuffix}
|
||||
results := []bool{false, false, true, false, false, true, true}
|
||||
var filecontent string
|
||||
var filebytes []byte
|
||||
for _, name := range names {
|
||||
if strings.Contains(name, validFileName+invalidSuffix+" GOCRYPTFS_BAD_NAME") {
|
||||
foundDecodable = true
|
||||
} else if strings.Contains(name, encryptedfilename[:len(encryptedfilename)-2]+invalidSuffix+" GOCRYPTFS_BAD_NAME") {
|
||||
foundUndecodable = true
|
||||
if name == searchstrings[0] {
|
||||
//Case 1: Test access
|
||||
filebytes, err = ioutil.ReadFile(mnt + "/" + name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
filecontent = string(filebytes)
|
||||
if filecontent == "Content Case 1." {
|
||||
results[0] = true
|
||||
}
|
||||
|
||||
} else if name == searchstrings[1] {
|
||||
//Case 2: Test Access
|
||||
filebytes, err = ioutil.ReadFile(mnt + "/" + name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
filecontent = string(filebytes)
|
||||
if filecontent == "Content Case 2." {
|
||||
results[1] = true
|
||||
}
|
||||
} else if name == searchstrings[3] {
|
||||
//Case 4: Test Access
|
||||
filebytes, err = ioutil.ReadFile(mnt + "/" + name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
filecontent = string(filebytes)
|
||||
if filecontent == "Content Case 4." {
|
||||
results[3] = true
|
||||
}
|
||||
} else if name == searchstrings[4] {
|
||||
//Case 5: Test Access
|
||||
filebytes, err = ioutil.ReadFile(mnt + "/" + name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
filecontent = string(filebytes)
|
||||
if filecontent == "Content Case 5." {
|
||||
results[4] = true
|
||||
}
|
||||
} else if name == searchstrings[6] {
|
||||
//Case 7
|
||||
results[6] = false
|
||||
}
|
||||
//Case 3 is always passed
|
||||
//Case 6 is highly obscure:
|
||||
//The last part of a valid cipher name must match the badname pattern AND
|
||||
//the remaining cipher name must still be decryptable. Test case not programmable in a general case
|
||||
}
|
||||
|
||||
if !foundDecodable {
|
||||
t.Errorf("did not find invalid name %s in %v", validFileName+invalidSuffix+" GOCRYPTFS_BAD_NAME", names)
|
||||
}
|
||||
|
||||
if !foundUndecodable {
|
||||
t.Errorf("did not find invalid name %s in %v", encryptedfilename[:len(encryptedfilename)-2]+invalidSuffix+" GOCRYPTFS_BAD_NAME", names)
|
||||
for i := 0; i < len(results); i++ {
|
||||
if !results[i] {
|
||||
t.Errorf("Case %d failed: '%s' in [%s]", i+1, searchstrings[i], strings.Join(names, ","))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/rfjakob/gocryptfs/tests/test_helpers"
|
||||
)
|
||||
|
||||
// TestZerokey verifies that `gocryptfs -zerokey` uses the same options as
|
||||
// `gocryptfs -init`.
|
||||
func TestZerokey(t *testing.T) {
|
||||
// Create FS
|
||||
dir := test_helpers.InitFS(t)
|
||||
|
||||
// Change masterkey to all-zero using password change
|
||||
args := []string{"-q", "-passwd", "-masterkey",
|
||||
"00000000-00000000-00000000-00000000-00000000-00000000-00000000-00000000"}
|
||||
args = append(args, dir)
|
||||
cmd := exec.Command(test_helpers.GocryptfsBinary, args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
p, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
// New password = old password
|
||||
p.Write([]byte("test\n"))
|
||||
p.Close()
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Add content
|
||||
mnt := dir + ".mnt"
|
||||
test_helpers.MountOrFatal(t, dir, mnt, "-extpass", "echo test")
|
||||
file1 := mnt + "/file1"
|
||||
err = ioutil.WriteFile(file1, []byte("somecontent"), 0600)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
test_helpers.UnmountPanic(mnt)
|
||||
|
||||
// Mount using -zerokey and verify we get the same result
|
||||
test_helpers.MountOrFatal(t, dir, mnt, "-extpass", "echo test")
|
||||
content, err := ioutil.ReadFile(file1)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else if string(content) != "somecontent" {
|
||||
t.Errorf("wrong content: %q", string(content))
|
||||
}
|
||||
test_helpers.UnmountPanic(mnt)
|
||||
}
|
|
@ -66,7 +66,7 @@ func TestCtlSockDecrypt(t *testing.T) {
|
|||
}
|
||||
response := test_helpers.QueryCtlSock(t, sock, req)
|
||||
if response.Result == "" || response.ErrNo != 0 {
|
||||
t.Fatalf("got an error reply: %+v", response)
|
||||
t.Fatalf("got an error for query %+v: %+v", req, response)
|
||||
}
|
||||
// Check if the encrypted path actually exists
|
||||
cPath := response.Result
|
||||
|
|
|
@ -18,6 +18,9 @@ import (
|
|||
|
||||
func TestMain(m *testing.M) {
|
||||
test_helpers.ResetTmpDir(true)
|
||||
// TestZerokey() in tests/cli verifies that mounting with `-zerokey` is equivalent
|
||||
// to mounting with a config file with all-default options (just the masterkey
|
||||
// set to all-zero).
|
||||
test_helpers.MountOrExit(test_helpers.DefaultCipherDir, test_helpers.DefaultPlainDir, "-zerokey")
|
||||
r := m.Run()
|
||||
test_helpers.UnmountPanic(test_helpers.DefaultPlainDir)
|
||||
|
@ -315,9 +318,9 @@ read(3, "M:\tSylwester Nawrocki <s.nawrock"..., 32768) = 32768
|
|||
read(3, "rs/scsi/eata*\n\nEATA ISA/EISA/PCI"..., 32768) = 32768
|
||||
read(3, "F:\tDocumentation/isapnp.txt\nF:\td"..., 32768) = 32768
|
||||
read(3, "hunkeey@googlemail.com>\nL:\tlinux"..., 32768) = 32768
|
||||
read(3, "ach-spear3xx/\n\nSPEAR6XX MACHINE "..., 32768) = 32768
|
||||
read(3, "ach-spear3xx/\n\nSPEAR6XX MACHINE "..., 32768) = 32768 <--- WRONG LENGTH!!!
|
||||
read(3, "", 32768) = 0
|
||||
lseek(3, 0, SEEK_CUR) = 196608
|
||||
lseek(3, 0, SEEK_CUR) = 196608 <--- WRONG LENGTH!!!
|
||||
close(3) = 0
|
||||
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x2), ...}) = 0
|
||||
write(1, "279b6ab0491e7532132e8f32afe6c04d"..., 56279b6ab0491e7532132e8f32afe6c04d linux-3.0/MAINTAINERS
|
||||
|
@ -370,3 +373,24 @@ func TestMd5sumMaintainers(t *testing.T) {
|
|||
t.Logf("full output:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxlen(t *testing.T) {
|
||||
workDir := filepath.Join(test_helpers.DefaultPlainDir, t.Name())
|
||||
if err := os.Mkdir(workDir, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := exec.Command("../../contrib/maxlen.bash", workDir)
|
||||
cmd.Env = []string{"QUICK=1"}
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Log(string(out))
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := `
|
||||
Maximum filename length: 255
|
||||
Maximum path length with 100 chars per subdir: 4095
|
||||
`
|
||||
if !strings.HasSuffix(string(out), want) {
|
||||
t.Errorf("wrong output: %s", string(out))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
#!/bin/bash
|
||||
exec ../../gocryptfs -fsck -extpass "echo test" broken_fs_v1.4
|
|
@ -119,7 +119,8 @@ func TestConcurrentReadCreate(t *testing.T) {
|
|||
}
|
||||
buf := buf0[:n]
|
||||
if bytes.Compare(buf, content) != 0 {
|
||||
log.Fatal("content mismatch")
|
||||
// Calling t.Fatal() from a goroutine hangs the test so we use log.Fatal
|
||||
log.Fatalf("%s: content mismatch: have=%q want=%q", t.Name(), string(buf), string(content))
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
#!/bin/bash -eu
|
||||
#
|
||||
# Find out the maximum supported filename length and print it.
|
||||
#
|
||||
# Part of the gocryptfs test suite
|
||||
# https://nuetzlich.net/gocryptfs/
|
||||
|
||||
NAME="maxlen."
|
||||
LEN=0
|
||||
|
||||
while [ $LEN -le 10000 ]; do
|
||||
touch $NAME 2> /dev/null || break
|
||||
rm $NAME
|
||||
LEN=${#NAME}
|
||||
NAME="${NAME}x"
|
||||
done
|
||||
|
||||
echo $LEN
|
|
@ -141,6 +141,8 @@ func isExt4(path string) bool {
|
|||
// gocryptfs -q -init -extpass "echo test" -scryptn=10 $extraArgs $cipherdir
|
||||
//
|
||||
// It returns cipherdir without a trailing slash.
|
||||
//
|
||||
// If t is set, t.Fatal() is called on error, log.Panic() otherwise.
|
||||
func InitFS(t *testing.T, extraArgs ...string) string {
|
||||
prefix := "x."
|
||||
if t != nil {
|
||||
|
|
Loading…
Reference in New Issue