diff --git a/cli_args.go b/cli_args.go index 358a6a9..5751f81 100644 --- a/cli_args.go +++ b/cli_args.go @@ -18,7 +18,7 @@ type argContainer struct { plaintextnames, quiet, nosyslog, wpanic, longnames, allow_other, ro, reverse, aessiv, nonempty, raw64 bool masterkey, mountpoint, cipherdir, cpuprofile, extpass, - memprofile, ko, passfile string + memprofile, ko, passfile, ctlsock string // Configuration file name override config string notifypid, scryptn int @@ -112,6 +112,7 @@ func parseCliOpts() (args argContainer) { flagSet.StringVar(&args.extpass, "extpass", "", "Use external program for the password prompt") flagSet.StringVar(&args.passfile, "passfile", "", "Read password from file") flagSet.StringVar(&args.ko, "ko", "", "Pass additional options directly to the kernel, comma-separated list") + flagSet.StringVar(&args.ctlsock, "ctlsock", "", "Create control socket at specified path") flagSet.IntVar(&args.notifypid, "notifypid", 0, "Send USR1 to the specified process after "+ "successful mount - used internally for daemonization") flagSet.IntVar(&args.scryptn, "scryptn", configfile.ScryptDefaultLogN, "scrypt cost parameter logN. "+ diff --git a/internal/ctlsock/ctlsock_serve.go b/internal/ctlsock/ctlsock_serve.go new file mode 100644 index 0000000..571260d --- /dev/null +++ b/internal/ctlsock/ctlsock_serve.go @@ -0,0 +1,137 @@ +// Package ctlsock implementes the control socket interface that can be +// activated by passing "-ctlsock" on the command line. +package ctlsock + +import ( + "encoding/json" + "errors" + "io" + "net" + "os" + "syscall" + + "github.com/rfjakob/gocryptfs/internal/tlog" +) + +// Interface should be implemented by fusefrontend[_reverse] +type Interface interface { + EncryptPath(string) (string, error) + DecryptPath(string) (string, error) +} + +// RequestStruct is sent by a client +type RequestStruct struct { + EncryptPath string + DecryptPath string +} + +// ResponseStruct is sent by us as response to a request +type ResponseStruct struct { + // Result is the resulting decrypted or encrypted path. Empty on error. + Result string + // ErrNo is the error number as defined in errno.h. + // 0 means success and -1 means that the error number is not known + // (look at ErrText in this case). + ErrNo int32 + // ErrText is a detailed error message. + ErrText string +} + +type ctlSockHandler struct { + fs Interface + socket *net.UnixListener +} + +// CreateAndServe creates an unix socket at "path" and serves incoming +// connections in a new goroutine. +func CreateAndServe(path string, fs Interface) error { + sock, err := net.Listen("unix", path) + if err != nil { + return err + } + handler := ctlSockHandler{ + fs: fs, + socket: sock.(*net.UnixListener), + } + go handler.acceptLoop() + return nil +} + +func (ch *ctlSockHandler) acceptLoop() { + for { + conn, err := ch.socket.Accept() + if err != nil { + tlog.Warn.Printf("ctlsock: Accept error: %v", err) + break + } + go ch.handleConnection(conn.(*net.UnixConn)) + } +} + +func (ch *ctlSockHandler) handleConnection(conn *net.UnixConn) { + // 2*PATH_MAX is definitely big enough for requests to decrypt or + // encrypt paths. + buf := make([]byte, 2*syscall.PathMax) + for { + n, err := conn.Read(buf) + if err == io.EOF { + conn.Close() + return + } else if err != nil { + tlog.Warn.Printf("ctlsock: Read error: %#v", err) + conn.Close() + return + } + buf = buf[:n] + var in RequestStruct + err = json.Unmarshal(buf, &in) + if err != nil { + tlog.Warn.Printf("ctlsock: Unmarshal error: %#v", err) + errorMsg := ResponseStruct{ + ErrNo: int32(syscall.EINVAL), + ErrText: err.Error(), + } + sendResponse(&errorMsg, conn) + } + ch.handleRequest(&in, conn) + // Restore original size. + buf = buf[:cap(buf)] + } +} + +func (ch *ctlSockHandler) handleRequest(in *RequestStruct, conn *net.UnixConn) { + var err error + var out ResponseStruct + if in.DecryptPath != "" && in.EncryptPath != "" { + err = errors.New("Ambigous") + } else if in.DecryptPath == "" && in.EncryptPath == "" { + err = errors.New("No operation") + } else if in.DecryptPath != "" { + out.Result, err = ch.fs.DecryptPath(in.DecryptPath) + } else if in.EncryptPath != "" { + out.Result, err = ch.fs.EncryptPath(in.EncryptPath) + } + if err != nil { + out.ErrText = err.Error() + out.ErrNo = -1 + // Try to extract the actual error number + if pe, ok := err.(*os.PathError); ok { + if se, ok := pe.Err.(syscall.Errno); ok { + out.ErrNo = int32(se) + } + } + } + sendResponse(&out, conn) +} + +func sendResponse(msg *ResponseStruct, conn *net.UnixConn) { + jsonMsg, err := json.Marshal(msg) + if err != nil { + tlog.Warn.Printf("ctlsock: Marshal failed: %v", err) + return + } + _, err = conn.Write(jsonMsg) + if err != nil { + tlog.Warn.Printf("ctlsock: Write failed: %v", err) + } +} diff --git a/internal/fusefrontend/ctlsock_interface.go b/internal/fusefrontend/ctlsock_interface.go new file mode 100644 index 0000000..c22dce6 --- /dev/null +++ b/internal/fusefrontend/ctlsock_interface.go @@ -0,0 +1,19 @@ +package fusefrontend + +import ( + "errors" + + "github.com/rfjakob/gocryptfs/internal/ctlsock" +) + +var _ ctlsock.Interface = &FS{} // Verify that interface is implemented. + +// EncryptPath implements ctlsock.Backend +func (fs *FS) EncryptPath(plainPath string) (string, error) { + return fs.encryptPath(plainPath) +} + +// DecryptPath implements ctlsock.Backend +func (fs *FS) DecryptPath(plainPath string) (string, error) { + return "", errors.New("Not implemented") +} diff --git a/internal/fusefrontend/write_lock.go b/internal/fusefrontend/write_lock.go index 7394994..3addfd6 100644 --- a/internal/fusefrontend/write_lock.go +++ b/internal/fusefrontend/write_lock.go @@ -21,7 +21,7 @@ var wlock wlockMap // 2) lock ... unlock ... // 3) unregister type wlockMap struct { - // Counts lock() calls. As every operation that modifies a file should + // opCount counts lock() calls. As every operation that modifies a file should // call it, this effectively serves as a write-operation counter. // The variable is accessed without holding any locks so atomic operations // must be used. It must be the first element of the struct to guarantee diff --git a/internal/fusefrontend_reverse/ctlsock_interface.go b/internal/fusefrontend_reverse/ctlsock_interface.go new file mode 100644 index 0000000..448663f --- /dev/null +++ b/internal/fusefrontend_reverse/ctlsock_interface.go @@ -0,0 +1,19 @@ +package fusefrontend_reverse + +import ( + "errors" + + "github.com/rfjakob/gocryptfs/internal/ctlsock" +) + +var _ ctlsock.Interface = &reverseFS{} // Verify that interface is implemented. + +// EncryptPath implements ctlsock.Backend +func (rfs *reverseFS) EncryptPath(plainPath string) (string, error) { + return "", errors.New("Not implemented") +} + +// DecryptPath implements ctlsock.Backend +func (rfs *reverseFS) DecryptPath(plainPath string) (string, error) { + return rfs.decryptPath(plainPath) +} diff --git a/internal/fusefrontend_reverse/rfs.go b/internal/fusefrontend_reverse/rfs.go index 87a2602..84348f7 100644 --- a/internal/fusefrontend_reverse/rfs.go +++ b/internal/fusefrontend_reverse/rfs.go @@ -49,7 +49,7 @@ var _ pathfs.FileSystem = &reverseFS{} // NewFS returns an encrypted FUSE overlay filesystem. // In this case (reverse mode) the backing directory is plain-text and // reverseFS provides an encrypted view. -func NewFS(args fusefrontend.Args) pathfs.FileSystem { +func NewFS(args fusefrontend.Args) *reverseFS { if args.CryptoBackend != cryptocore.BackendAESSIV { panic("reverse mode must use AES-SIV, everything else is insecure") } diff --git a/mount.go b/mount.go index 1a00bb7..3bafd8c 100644 --- a/mount.go +++ b/mount.go @@ -18,6 +18,7 @@ import ( "github.com/rfjakob/gocryptfs/internal/configfile" "github.com/rfjakob/gocryptfs/internal/cryptocore" + "github.com/rfjakob/gocryptfs/internal/ctlsock" "github.com/rfjakob/gocryptfs/internal/fusefrontend" "github.com/rfjakob/gocryptfs/internal/fusefrontend_reverse" "github.com/rfjakob/gocryptfs/internal/tlog" @@ -165,10 +166,18 @@ func initFuseFrontend(key []byte, args *argContainer, confFile *configfile.ConfF jsonBytes, _ := json.MarshalIndent(frontendArgs, "", "\t") tlog.Debug.Printf("frontendArgs: %s", string(jsonBytes)) var finalFs pathfs.FileSystem + var ctlSockBackend ctlsock.Interface if args.reverse { - finalFs = fusefrontend_reverse.NewFS(frontendArgs) + fs := fusefrontend_reverse.NewFS(frontendArgs) + finalFs = fs + ctlSockBackend = fs } else { - finalFs = fusefrontend.NewFS(frontendArgs) + fs := fusefrontend.NewFS(frontendArgs) + finalFs = fs + ctlSockBackend = fs + } + if args.ctlsock != "" { + ctlsock.CreateAndServe(args.ctlsock, ctlSockBackend) } pathFsOpts := &pathfs.PathNodeFsOptions{ClientInodes: true} pathFs := pathfs.NewPathNodeFs(finalFs, pathFsOpts) @@ -200,9 +209,8 @@ func initFuseFrontend(key []byte, args *argContainer, confFile *configfile.ConfF if args.reverse { mOpts.Name += "-reverse" } - // The kernel enforces read-only operation, we just have to pass "ro". - // Reverse mounts are always read-only + // Reverse mounts are always read-only. if args.ro || args.reverse { mOpts.Options = append(mOpts.Options, "ro") } diff --git a/tests/defaults/main_test.go b/tests/defaults/main_test.go index 849aa69..8733c8b 100644 --- a/tests/defaults/main_test.go +++ b/tests/defaults/main_test.go @@ -2,10 +2,16 @@ package defaults import ( + "encoding/json" + "fmt" + "net" "os" "os/exec" + "syscall" "testing" + "time" + "github.com/rfjakob/gocryptfs/internal/ctlsock" "github.com/rfjakob/gocryptfs/tests/test_helpers" ) @@ -35,3 +41,34 @@ func Test1980Tar(t *testing.T) { t.Errorf("Wrong mtime: %d", m) } } + +func TestCtlSock(t *testing.T) { + cDir := test_helpers.InitFS(t) + pDir := cDir + ".mnt" + sock := cDir + ".sock" + test_helpers.MountOrFatal(t, cDir, pDir, "-ctlsock="+sock, "-extpass", "echo test") + defer test_helpers.UnmountPanic(pDir) + conn, err := net.DialTimeout("unix", sock, 1*time.Second) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + conn.SetDeadline(time.Now().Add(time.Second)) + msg := []byte(`{"EncryptPath": "foobar"}`) + _, err = conn.Write(msg) + if err != nil { + t.Fatal(err) + } + buf := make([]byte, 2*syscall.PathMax) + n, err := conn.Read(buf) + if err != nil { + t.Fatal(err) + } + buf = buf[:n] + var response ctlsock.ResponseStruct + json.Unmarshal(buf, &response) + if response.Result == "" || response.ErrNo != 0 { + fmt.Printf("%s\n", string(buf)) + t.Errorf("got an error reply") + } +}