libgocryptfs/main.go
Jakob Unterwurzacher 09e88f31d1 toggledlog: assume command of color handling
We want to use colored error messages also outside of main,
so let's handle it in the logging package.

The fatal logger now automatically prints red.
2016-06-15 23:17:51 +02:00

455 lines
16 KiB
Go

package main
import (
"encoding/json"
"flag"
"fmt"
"log/syslog"
"os"
"os/exec"
"os/signal"
"path/filepath"
"runtime"
"runtime/pprof"
"strconv"
"strings"
"syscall"
"time"
"github.com/hanwen/go-fuse/fuse"
"github.com/hanwen/go-fuse/fuse/nodefs"
"github.com/hanwen/go-fuse/fuse/pathfs"
"github.com/rfjakob/gocryptfs/internal/configfile"
"github.com/rfjakob/gocryptfs/internal/contentenc"
"github.com/rfjakob/gocryptfs/internal/cryptocore"
"github.com/rfjakob/gocryptfs/internal/fusefrontend"
"github.com/rfjakob/gocryptfs/internal/nametransform"
"github.com/rfjakob/gocryptfs/internal/prefer_openssl"
"github.com/rfjakob/gocryptfs/internal/readpassword"
"github.com/rfjakob/gocryptfs/internal/toggledlog"
)
const (
// Exit codes
ERREXIT_USAGE = 1
ERREXIT_MOUNT = 3
ERREXIT_CIPHERDIR = 6
ERREXIT_INIT = 7
ERREXIT_LOADCONF = 8
ERREXIT_MOUNTPOINT = 10
)
type argContainer struct {
debug, init, zerokey, fusedebug, openssl, passwd, foreground, version,
plaintextnames, quiet, diriv, emenames, gcmiv128, nosyslog, wpanic,
longnames, allow_other bool
masterkey, mountpoint, cipherdir, cpuprofile, config, extpass,
memprofile string
notifypid, scryptn int
}
var flagSet *flag.FlagSet
// GitVersion will be set by the build script "build.bash"
var GitVersion = "[version not set - please compile using ./build.bash]"
var GitVersionFuse = "[version not set - please compile using ./build.bash]"
// initDir initializes an empty directory for use as a gocryptfs cipherdir.
func initDir(args *argContainer) {
err := checkDirEmpty(args.cipherdir)
if err != nil {
toggledlog.Fatal.Printf("Invalid cipherdir: %v", err)
os.Exit(ERREXIT_INIT)
}
// Create gocryptfs.conf
if args.extpass == "" {
toggledlog.Info.Printf("Choose a password for protecting your files.")
} else {
toggledlog.Info.Printf("Using password provided via -extpass.")
}
password := readpassword.Twice(args.extpass)
creator := toggledlog.ProgramName + " " + GitVersion
err = configfile.CreateConfFile(args.config, password, args.plaintextnames, args.scryptn, creator)
if err != nil {
toggledlog.Fatal.Println(err)
os.Exit(ERREXIT_INIT)
}
if args.diriv && !args.plaintextnames {
// Create gocryptfs.diriv in the root dir
err = nametransform.WriteDirIV(args.cipherdir)
if err != nil {
toggledlog.Fatal.Println(err)
os.Exit(ERREXIT_INIT)
}
}
toggledlog.Info.Printf(toggledlog.ColorGreen + "The filesystem has been created successfully." + toggledlog.ColorReset)
wd, _ := os.Getwd()
friendlyPath, _ := filepath.Rel(wd, args.cipherdir)
if strings.HasPrefix(friendlyPath, "../") {
// A relative path that starts with "../" is pretty unfriendly, just
// keep the absolute path.
friendlyPath = args.cipherdir
}
toggledlog.Info.Printf(toggledlog.ColorGrey+"You can now mount it using: %s %s MOUNTPOINT"+toggledlog.ColorReset,
toggledlog.ProgramName, friendlyPath)
os.Exit(0)
}
func usageText() {
printVersion()
fmt.Printf(`
Usage: %s -init|-passwd [OPTIONS] CIPHERDIR
or %s [OPTIONS] CIPHERDIR MOUNTPOINT
Options:
`, toggledlog.ProgramName, toggledlog.ProgramName)
flagSet.PrintDefaults()
}
// loadConfig - load the config file "filename", prompting the user for the password
func loadConfig(args *argContainer) (masterkey []byte, confFile *configfile.ConfFile) {
// Check if the file exists at all before prompting for a password
_, err := os.Stat(args.config)
if err != nil {
toggledlog.Fatal.Printf("Config file not found: %v", err)
os.Exit(ERREXIT_LOADCONF)
}
pw := readpassword.Once(args.extpass)
toggledlog.Info.Println("Decrypting master key")
masterkey, confFile, err = configfile.LoadConfFile(args.config, pw)
if err != nil {
toggledlog.Fatal.Println(err.Error())
os.Exit(ERREXIT_LOADCONF)
}
return masterkey, confFile
}
// changePassword - change the password of config file "filename"
func changePassword(args *argContainer) {
masterkey, confFile := loadConfig(args)
toggledlog.Info.Println("Please enter your new password.")
newPw := readpassword.Twice(args.extpass)
confFile.EncryptKey(masterkey, newPw, confFile.ScryptObject.LogN())
err := confFile.WriteFile()
if err != nil {
toggledlog.Fatal.Println(err)
os.Exit(ERREXIT_INIT)
}
toggledlog.Info.Printf("Password changed.")
os.Exit(0)
}
// printVersion - print a version string like
// "gocryptfs v0.3.1-31-g6736212-dirty; on-disk format 2"
func printVersion() {
fmt.Printf("%s %s; on-disk format %d; go-fuse %s\n",
toggledlog.ProgramName, GitVersion, contentenc.CurrentVersion, GitVersionFuse)
}
func main() {
runtime.GOMAXPROCS(4)
var err error
var args argContainer
// Parse command line arguments
var opensslAuto string
flagSet = flag.NewFlagSet(toggledlog.ProgramName, flag.ExitOnError)
flagSet.Usage = usageText
flagSet.BoolVar(&args.debug, "d", false, "")
flagSet.BoolVar(&args.debug, "debug", false, "Enable debug output")
flagSet.BoolVar(&args.fusedebug, "fusedebug", false, "Enable fuse library debug output")
flagSet.BoolVar(&args.init, "init", false, "Initialize encrypted directory")
flagSet.BoolVar(&args.zerokey, "zerokey", false, "Use all-zero dummy master key")
// Tri-state true/false/auto
flagSet.StringVar(&opensslAuto, "openssl", "auto", "Use OpenSSL instead of built-in Go crypto")
flagSet.BoolVar(&args.passwd, "passwd", false, "Change password")
flagSet.BoolVar(&args.foreground, "f", false, "Stay in the foreground")
flagSet.BoolVar(&args.version, "version", false, "Print version and exit")
flagSet.BoolVar(&args.plaintextnames, "plaintextnames", false, "Do not encrypt file names")
flagSet.BoolVar(&args.quiet, "q", false, "")
flagSet.BoolVar(&args.quiet, "quiet", false, "Quiet - silence informational messages")
flagSet.BoolVar(&args.diriv, "diriv", true, "Use per-directory file name IV")
flagSet.BoolVar(&args.emenames, "emenames", true, "Use EME filename encryption. This option implies diriv.")
flagSet.BoolVar(&args.gcmiv128, "gcmiv128", true, "Use an 128-bit IV for GCM encryption instead of Go's default of 96 bits")
flagSet.BoolVar(&args.nosyslog, "nosyslog", false, "Do not redirect output to syslog when running in the background")
flagSet.BoolVar(&args.wpanic, "wpanic", false, "When encountering a warning, panic and exit immediately")
flagSet.BoolVar(&args.longnames, "longnames", true, "Store names longer than 176 bytes in extra files")
flagSet.BoolVar(&args.allow_other, "allow_other", false, "Allow other users to access the filesystem. "+
"Only works if user_allow_other is set in /etc/fuse.conf.")
flagSet.StringVar(&args.masterkey, "masterkey", "", "Mount with explicit master key")
flagSet.StringVar(&args.cpuprofile, "cpuprofile", "", "Write cpu profile to specified file")
flagSet.StringVar(&args.memprofile, "memprofile", "", "Write memory profile to specified file")
flagSet.StringVar(&args.config, "config", "", "Use specified config file instead of CIPHERDIR/gocryptfs.conf")
flagSet.StringVar(&args.extpass, "extpass", "", "Use external program for the password prompt")
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. "+
"Setting this to a lower value speeds up mounting but makes the password susceptible to brute-force attacks")
flagSet.Parse(os.Args[1:])
// "-openssl" needs some post-processing
if opensslAuto == "auto" {
args.openssl = prefer_openssl.PreferOpenSSL()
} else {
args.openssl, err = strconv.ParseBool(opensslAuto)
if err != nil {
toggledlog.Fatal.Printf("Invalid \"-openssl\" setting: %v", err)
os.Exit(ERREXIT_USAGE)
}
}
// Fork a child into the background if "-f" is not set AND we are mounting a filesystem
if !args.foreground && flagSet.NArg() == 2 {
forkChild() // does not return
}
if args.debug {
toggledlog.Debug.Enabled = true
}
// "-v"
if args.version {
toggledlog.Debug.Printf("openssl=%v\n", args.openssl)
printVersion()
os.Exit(0)
}
if args.wpanic {
toggledlog.Warn.Wpanic = true
toggledlog.Debug.Printf("Panicing on warnings")
}
// Every operation below requires CIPHERDIR. Check that we have it.
if flagSet.NArg() >= 1 {
args.cipherdir, _ = filepath.Abs(flagSet.Arg(0))
err = checkDir(args.cipherdir)
if err != nil {
toggledlog.Fatal.Printf("Invalid cipherdir: %v", err)
os.Exit(ERREXIT_CIPHERDIR)
}
} else {
usageText()
os.Exit(ERREXIT_USAGE)
}
// "-q"
if args.quiet {
toggledlog.Info.Enabled = false
}
// "-config"
if args.config != "" {
args.config, err = filepath.Abs(args.config)
if err != nil {
toggledlog.Fatal.Printf("Invalid \"-config\" setting: %v", err)
os.Exit(ERREXIT_INIT)
}
toggledlog.Info.Printf("Using config file at custom location %s", args.config)
} else {
args.config = filepath.Join(args.cipherdir, configfile.ConfDefaultName)
}
// "-cpuprofile"
if args.cpuprofile != "" {
toggledlog.Info.Printf("Writing CPU profile to %s", args.cpuprofile)
var f *os.File
f, err = os.Create(args.cpuprofile)
if err != nil {
toggledlog.Fatal.Println(err)
os.Exit(ERREXIT_INIT)
}
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
}
// "-memprofile"
if args.memprofile != "" {
toggledlog.Info.Printf("Writing mem profile to %s", args.memprofile)
var f *os.File
f, err = os.Create(args.memprofile)
if err != nil {
toggledlog.Fatal.Println(err)
os.Exit(ERREXIT_INIT)
}
defer func() {
pprof.WriteHeapProfile(f)
f.Close()
return
}()
}
if args.cpuprofile != "" || args.memprofile != "" {
toggledlog.Info.Printf("Note: You must unmount gracefully, otherwise the profile file(s) will stay empty!\n")
}
// "-openssl"
if args.openssl == false {
toggledlog.Debug.Printf("OpenSSL disabled, using Go GCM")
} else {
toggledlog.Debug.Printf("OpenSSL enabled")
}
// Operation flags: init, passwd or mount
// "-init"
if args.init {
if flagSet.NArg() > 1 {
toggledlog.Fatal.Printf("Usage: %s -init [OPTIONS] CIPHERDIR", toggledlog.ProgramName)
os.Exit(ERREXIT_USAGE)
}
initDir(&args) // does not return
}
// "-passwd"
if args.passwd {
if flagSet.NArg() > 1 {
toggledlog.Fatal.Printf("Usage: %s -passwd [OPTIONS] CIPHERDIR", toggledlog.ProgramName)
os.Exit(ERREXIT_USAGE)
}
changePassword(&args) // does not return
}
// Mount
// Check mountpoint
if flagSet.NArg() != 2 {
toggledlog.Fatal.Printf("Usage: %s [OPTIONS] CIPHERDIR MOUNTPOINT", toggledlog.ProgramName)
os.Exit(ERREXIT_USAGE)
}
args.mountpoint, err = filepath.Abs(flagSet.Arg(1))
if err != nil {
toggledlog.Fatal.Printf("Invalid mountpoint: %v", err)
os.Exit(ERREXIT_MOUNTPOINT)
}
err = checkDirEmpty(args.mountpoint)
if err != nil {
toggledlog.Fatal.Printf("Invalid mountpoint: %v", err)
os.Exit(ERREXIT_MOUNTPOINT)
}
// Get master key
var masterkey []byte
var confFile *configfile.ConfFile
if args.masterkey != "" {
// "-masterkey"
toggledlog.Info.Printf("Using explicit master key.")
masterkey = parseMasterKey(args.masterkey)
toggledlog.Info.Printf("THE MASTER KEY IS VISIBLE VIA \"ps -auxwww\", ONLY USE THIS MODE FOR EMERGENCIES.")
} else if args.zerokey {
// "-zerokey"
toggledlog.Info.Printf("Using all-zero dummy master key.")
toggledlog.Info.Printf("ZEROKEY MODE PROVIDES NO SECURITY AT ALL AND SHOULD ONLY BE USED FOR TESTING.")
masterkey = make([]byte, cryptocore.KeyLen)
} else {
// Load master key from config file
masterkey, confFile = loadConfig(&args)
printMasterKey(masterkey)
}
// Initialize FUSE server
toggledlog.Debug.Printf("cli args: %v", args)
srv := initFuseFrontend(masterkey, args, confFile)
toggledlog.Info.Println(toggledlog.ColorGreen + "Filesystem mounted and ready." + toggledlog.ColorReset)
// We are ready - send USR1 signal to our parent and switch to syslog
if args.notifypid > 0 {
sendUsr1(args.notifypid)
if !args.nosyslog {
toggledlog.Info.SwitchToSyslog(syslog.LOG_USER | syslog.LOG_INFO)
toggledlog.Debug.SwitchToSyslog(syslog.LOG_USER | syslog.LOG_DEBUG)
toggledlog.Warn.SwitchToSyslog(syslog.LOG_USER | syslog.LOG_WARNING)
}
}
// Wait for SIGINT in the background and unmount ourselves if we get it.
// This prevents a dangling "Transport endpoint is not connected" mountpoint.
handleSigint(srv, args.mountpoint)
// Jump into server loop. Returns when it gets an umount request from the kernel.
srv.Serve()
// main exits with code 0
}
// initFuseFrontend - initialize gocryptfs/fusefrontend
// Calls os.Exit on errors
func initFuseFrontend(key []byte, args argContainer, confFile *configfile.ConfFile) *fuse.Server {
// Reconciliate CLI and config file arguments into a Args struct that is passed to the
// filesystem implementation
frontendArgs := fusefrontend.Args{
Cipherdir: args.cipherdir,
Masterkey: key,
OpenSSL: args.openssl,
PlaintextNames: args.plaintextnames,
DirIV: args.diriv,
EMENames: args.emenames,
GCMIV128: args.gcmiv128,
LongNames: args.longnames,
}
// confFile is nil when "-zerokey" or "-masterkey" was used
if confFile != nil {
// Settings from the config file override command line args
frontendArgs.PlaintextNames = confFile.IsFeatureFlagSet(configfile.FlagPlaintextNames)
frontendArgs.DirIV = confFile.IsFeatureFlagSet(configfile.FlagDirIV)
frontendArgs.EMENames = confFile.IsFeatureFlagSet(configfile.FlagEMENames)
frontendArgs.GCMIV128 = confFile.IsFeatureFlagSet(configfile.FlagGCMIV128)
}
// EMENames implies DirIV, both on the command line and in the config file.
if frontendArgs.EMENames {
frontendArgs.DirIV = true
}
// PlainTexnames disables both EMENames and DirIV
if frontendArgs.PlaintextNames {
frontendArgs.DirIV = false
frontendArgs.EMENames = false
}
jsonBytes, _ := json.MarshalIndent(frontendArgs, "", "\t")
toggledlog.Debug.Printf("frontendArgs: %s", string(jsonBytes))
finalFs := fusefrontend.NewFS(frontendArgs)
pathFsOpts := &pathfs.PathNodeFsOptions{ClientInodes: true}
pathFs := pathfs.NewPathNodeFs(finalFs, pathFsOpts)
fuseOpts := &nodefs.Options{
// These options are to be compatible with libfuse defaults,
// making benchmarking easier.
NegativeTimeout: time.Second,
AttrTimeout: time.Second,
EntryTimeout: time.Second,
}
conn := nodefs.NewFileSystemConnector(pathFs.Root(), fuseOpts)
var mOpts fuse.MountOptions
mOpts.AllowOther = false
if args.allow_other {
toggledlog.Info.Printf(toggledlog.ColorYellow + "The option \"-allow_other\" is set. Make sure the file " +
"permissions protect your data from unwanted access." + toggledlog.ColorReset)
mOpts.AllowOther = true
// Make the kernel check the file permissions for us
mOpts.Options = append(mOpts.Options, "default_permissions")
}
// Set values shown in "df -T" and friends
// First column, "Filesystem"
mOpts.Options = append(mOpts.Options, "fsname="+args.cipherdir)
// Second column, "Type", will be shown as "fuse." + Name
mOpts.Name = "gocryptfs"
srv, err := fuse.NewServer(conn.RawFS(), args.mountpoint, &mOpts)
if err != nil {
toggledlog.Fatal.Printf("Mount failed: %v", err)
os.Exit(ERREXIT_MOUNT)
}
srv.SetDebug(args.fusedebug)
// All FUSE file and directory create calls carry explicit permission
// information. We need an unrestricted umask to create the files and
// directories with the requested permissions.
syscall.Umask(0000)
return srv
}
func handleSigint(srv *fuse.Server, mountpoint string) {
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt)
signal.Notify(ch, syscall.SIGTERM)
go func() {
<-ch
err := srv.Unmount()
if err != nil {
toggledlog.Warn.Print(err)
toggledlog.Info.Printf("Trying lazy unmount")
cmd := exec.Command("fusermount", "-u", "-z", mountpoint)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Run()
}
os.Exit(1)
}()
}