Compare commits

...

32 Commits

Author SHA1 Message Date
Jakob Unterwurzacher a2b54cfccd root_test: fix leftover loop mount 5 months ago
Jakob Unterwurzacher ba75aa1ab0 root_test: add TestOverlay ; syscallcompat: add QuirkNoUserXattr 5 months ago
Jakob Unterwurzacher b636f79f89 MANPAGE: add missing -acl section 5 months ago
Jakob Unterwurzacher 47358938ec tests: fix build failure on Go 1.15 and older 5 months ago
Jakob Unterwurzacher 696f11499b tests: add skipped O_TMPFILE test 5 months ago
Jakob Unterwurzacher b859bc96ef fusefrontend: fix "duplicate case" darwin build failure 5 months ago
Jakob Unterwurzacher 3bac814ea9 tests: add TestRenameWhiteout, TestRenameExchange 5 months ago
Jakob Unterwurzacher b7cac4ffd0 fusefrontend: support RENAME_WHITEOUT, RENAME_EXCHANGE 5 months ago
Jakob Unterwurzacher 3ca2b1983d tests: enable -fusedebug if FUSEDEBUG env is set 5 months ago
Jakob Unterwurzacher 5f955423b7 fusefrontend: fix -force_owner not affecting MKNOD 6 months ago
Jakob Unterwurzacher c23a7f2259 test.bash: disable parallelism in verbose mode 6 months ago
Jakob Unterwurzacher 700ae685cc tests: improve SEEK_DATA test for MacOS 6 months ago
Jakob Unterwurzacher 4b251f3ce1 readpassword: bubble up errors instead of exiting the process 6 months ago
Jakob Unterwurzacher 1eaf1211a2 tests/cli: Check for leftover socket file 6 months ago
Jakob Unterwurzacher 5749e70c7c nametransform: fix oversight in comment 7 months ago
Jakob Unterwurzacher 8d8b76dcac go.mod: upgrade go-fuse 7 months ago
Jakob Unterwurzacher 64be5de75f fusefrontend: allow slashes in xattr names 7 months ago
Jakob Unterwurzacher eb42e54182 tlog: only enable color if both stderr and stdout are a terminal 7 months ago
Jakob Unterwurzacher a1f01419e2 tlog: respect NO_COLOR 7 months ago
Jakob Unterwurzacher bd1ecf5379 darwin: use O_NOFOLLOW for xattr opens 7 months ago
Jakob Unterwurzacher a48d6c3041 test.bash, crossbuild: catch MacOS test build failures 7 months ago
Jakob Unterwurzacher ec186c13ce root_test, getdents-debug: restrict to linux 7 months ago
Jakob Unterwurzacher 7d60315cd5 tests: convert Creat() calls to Open() 7 months ago
Jakob Unterwurzacher de22cb1e5d crossbuild.bash: use shell function instead of variable 7 months ago
Jakob Unterwurzacher 39e736c099 MANPAGE: fix typo 7 months ago
Jakob Unterwurzacher 8722b894a6 MANPAGE: -extpass: document dash duplication bug 7 months ago
Jakob Unterwurzacher d530fbd400 docs: names longer than 175 bytes (not 176) are stored in longnames 8 months ago
Jakob Unterwurzacher 87a6bb370a nametransform: fix math.MaxInt build failure on older Go 9 months ago
Jakob Unterwurzacher d14c9340d6 cli: add -longnamemax 9 months ago
Jakob Unterwurzacher d583bdb79e configfile: add LongNameMax support 9 months ago
Jakob Unterwurzacher dc32710045 nametransform: add longNameMax parameter 9 months ago
Jakob Unterwurzacher a652be805e configfile: replace broken switch/case logic with if 9 months ago
  1. 67
      Documentation/MANPAGE.md
  2. 6
      README.md
  3. 14
      cli_args.go
  4. 11
      cli_args_test.go
  5. 2
      contrib/getdents-debug/getdents/getdents.go
  6. 30
      crossbuild.bash
  7. 2
      go.mod
  8. 2
      go.sum
  9. 6
      gocryptfs-xray/xray_main.go
  10. 10
      init_dir.go
  11. 9
      internal/configfile/config_file.go
  12. 24
      internal/configfile/config_test.go
  13. 6
      internal/configfile/feature_flags.go
  14. 37
      internal/configfile/validate.go
  15. 29
      internal/fusefrontend/node.go
  16. 4
      internal/fusefrontend/node_xattr.go
  17. 12
      internal/fusefrontend/node_xattr_darwin.go
  18. 4
      internal/fusefrontend/root_node.go
  19. 2
      internal/fusefrontend/xattr_unit_test.go
  20. 2
      internal/nametransform/badname.go
  21. 41
      internal/nametransform/longnames_test.go
  22. 62
      internal/nametransform/names.go
  23. 23
      internal/nametransform/names_test.go
  24. 47
      internal/nametransform/xattr.go
  25. 73
      internal/readpassword/extpass_test.go
  26. 28
      internal/readpassword/passfile.go
  27. 76
      internal/readpassword/passfile_test.go
  28. 75
      internal/readpassword/read.go
  29. 33
      internal/readpassword/stdin_test.go
  30. 2
      internal/syscallcompat/quirks.go
  31. 4
      internal/syscallcompat/quirks_linux.go
  32. 9
      internal/syscallcompat/sys_darwin.go
  33. 4
      internal/syscallcompat/sys_linux.go
  34. 6
      internal/tlog/log.go
  35. 14
      main.go
  36. 8
      masterkey.go
  37. 4
      mount.go
  38. 41
      test.bash
  39. 28
      tests/cli/cli_test.go
  40. 61
      tests/cli/longnamemax_test.go
  41. 35
      tests/defaults/main_test.go
  42. 129
      tests/defaults/overlayfs_test.go
  43. 2
      tests/matrix/concurrency_test.go
  44. 2
      tests/plaintextnames/plaintextnames_test.go
  45. 14
      tests/reverse/correctness_test.go
  46. 43
      tests/root_test/root_test.go
  47. 4
      tests/sharedstorage/sharedstorage_test.go
  48. 5
      tests/test_helpers/mount_unmount.go
  49. 14
      tests/xattr/xattr_integration_test.go

67
Documentation/MANPAGE.md

@ -123,6 +123,28 @@ and https://github.com/rfjakob/gocryptfs/issues/596 for background info.
Use HKDF to derive separate keys for content and name encryption from
the master key. Default true.
#### -longnamemax
integer value, allowed range 62...255
Hash file names that (in encrypted form) exceed this length. The default
is 255, which aligns with the usual name length limit on Linux and
provides best performance.
However, online storage may impose lower limits on file name and/or
path length. In this case, setting -longnamemax to a lower value
can be helpful.
The lower the value, the more extra `.name` files
must be created, which slows down directory listings.
Values below 62 are not allowed as then the hashed name
would be longer than the original name.
Example:
-longnamemax 100
#### -plaintextnames
Do not encrypt file names and symlink targets.
@ -155,6 +177,10 @@ MOUNT OPTIONS
Available options for mounting are listed below. Usually, you don't need any.
Defaults are fine.
#### -acl
Enable ACL enforcement. When you want to use ACLs, you must enable this
option.
#### -allow_other
By default, the Linux kernel prevents any other user (even root) to
access a mounted FUSE filesystem. Settings this option allows access for
@ -296,9 +322,10 @@ the directories. Example:
gocryptfs -ko noexec /tmp/foo /tmp/bar
#### -longnames
Store names longer than 176 bytes in extra files (default true)
This flag is useful when recovering old gocryptfs filesystems using
"-masterkey". It is ignored (stays at the default) otherwise.
Store names that are longer than 175 bytes in extra files (default true).
This flag is only useful when recovering very old gocryptfs filesystems (gocryptfs v0.8 and earlier)
using "-masterkey". It is ignored (stays at the default) otherwise.
#### -nodev
See `-dev, -nodev`.
@ -452,6 +479,10 @@ to your program, use `"--"`, which is accepted by most programs:
Applies to: all actions that ask for a password.
BUG: In `-extpass -X`, the `-X` will be interpreted as `--X`. Please use
`-extpass=-X` to prevent that. See **Dash duplication** in the **BUGS** section
for details.
#### -fido2 DEVICE_PATH
Use a FIDO2 token to initialize and unlock the filesystem.
Use "fido2-token -L" to obtain the FIDO2 token device path.
@ -646,6 +677,13 @@ the `-nofail` option for details).
/tmp/cipher /tmp/plain fuse./usr/local/bin/gocryptfs nofail,allow_other,passfile=/tmp/password 0 0
ENVIRONMENT VARIABLES
=====================
### NO_COLOR
If `NO_COLOR` is set (regardless of value), colored output is disabled (see https://no-color.org/).
EXIT CODES
==========
@ -661,6 +699,29 @@ other: please check the error message
See also: https://github.com/rfjakob/gocryptfs/blob/master/internal/exitcodes/exitcodes.go
BUGS
====
### Dash duplication
gocryptfs v2.1 switched to the `pflag` library for command-line parsing
to support flags and positional arguments in any order. To stay compatible
with single-dash long options like `-extpass`, an ugly hack was added:
The command line is preprocessed, and all single-dash options are converted to
double-dash.
Unfortunately, this means that in
gocryptfs -extpass myapp -extpass -X
gocryptfs transforms the `-X` to `--X`, and it will call `myapp --X` as the extpass program.
Please use
gocryptfs -extpass myapp -extpass=-X
to work around this bug.
SEE ALSO
========
mount(2) fuse(8) fallocate(2) encfs(1) gitignore(5)

6
README.md

@ -196,6 +196,12 @@ RM: 2,367
Changelog
---------
#### vNEXT
* Add **`-longnamemax`** flag to `-init` ([#499](https://github.com/rfjakob/gocryptfs/issues/499)).
Can be used to work around file or path length restrictions on online storage.
See the [man page](https://github.com/rfjakob/gocryptfs/blob/master/Documentation/MANPAGE.md#-longnamemax)
for details.
#### v2.2.1, 2021-10-20
* Fix `-force_owner` only taking effect after 2 seconds ([#609](https://github.com/rfjakob/gocryptfs/issues/609)).
This was a regression introduced in v2.0.

14
cli_args.go

@ -45,6 +45,8 @@ type argContainer struct {
notifypid, scryptn int
// Idle time before autounmount
idle time.Duration
// -longnamemax (hash encrypted names that are longer than this)
longnamemax uint8
// Helper variables that are NOT cli options all start with an underscore
// _configCustom is true when the user sets a custom config file name.
_configCustom bool
@ -112,6 +114,10 @@ func prefixOArgs(osArgs []string) ([]string, error) {
// into "--debug" (spf13/pflag style).
// gocryptfs v2.1 switched from `flag` to `pflag`, but we obviously want to stay
// cli-compatible, and this is the hack to do it.
//
// BUG: In `-extpass -X`, the `-X` gets transformed `--X`.
// See "Dash duplication" in the man page and
// https://github.com/rfjakob/gocryptfs/issues/621 .
func convertToDoubleDash(osArgs []string) (out []string) {
out = append(out, osArgs...)
for i, v := range out {
@ -161,7 +167,7 @@ func parseCliOpts(osArgs []string) (args argContainer) {
flagSet.BoolVar(&args.quiet, "quiet", false, "Quiet - silence informational messages")
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.longnames, "longnames", true, "Store names longer than 175 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.BoolVar(&args.reverse, "reverse", false, "Reverse mode")
@ -215,6 +221,8 @@ func parseCliOpts(osArgs []string) (args argContainer) {
flagSet.StringSliceVar(&args.badname, "badname", nil, "Glob pattern invalid file names that should be shown")
flagSet.StringSliceVar(&args.passfile, "passfile", nil, "Read password from file")
flagSet.Uint8Var(&args.longnamemax, "longnamemax", 255, "Hash encrypted names that are longer than this")
flagSet.IntVar(&args.notifypid, "notifypid", 0, "Send USR1 to the specified process after "+
"successful mount - used internally for daemonization")
const scryptn = "scryptn"
@ -292,6 +300,10 @@ func parseCliOpts(osArgs []string) (args argContainer) {
os.Exit(exitcodes.Usage)
}
}
if args.longnamemax > 0 && args.longnamemax < 62 {
tlog.Fatal.Printf("-longnamemax: value %d is outside allowed range 62 ... 255", args.longnamemax)
os.Exit(exitcodes.Usage)
}
return args
}

11
cli_args_test.go

@ -116,11 +116,12 @@ func TestConvertToDoubleDash(t *testing.T) {
func TestParseCliOpts(t *testing.T) {
defaultArgs := argContainer{
longnames: true,
raw64: true,
hkdf: true,
openssl: stupidgcm.PreferOpenSSLAES256GCM(), // depends on CPU and build flags
scryptn: 16,
longnames: true,
longnamemax: 255,
raw64: true,
hkdf: true,
openssl: stupidgcm.PreferOpenSSLAES256GCM(), // depends on CPU and build flags
scryptn: 16,
}
type testcaseContainer struct {

2
contrib/getdents-debug/getdents/getdents.go

@ -1,3 +1,5 @@
//+build linux
/*
Small tool to try to debug unix.Getdents problems on CIFS mounts
( https://github.com/rfjakob/gocryptfs/issues/483 )

30
crossbuild.bash

@ -1,29 +1,37 @@
#!/bin/bash -eu
#!/bin/bash
#
# Build on all supported architectures & operating systems
cd "$(dirname "$0")"
function build {
# Discard resulting binary by writing to /dev/null
go build -tags without_openssl -o /dev/null
}
export GO111MODULE=on
# Discard resulting binary by writing to /dev/null
B="go build -tags without_openssl -o /dev/null"
function compile_tests {
for i in $(go list ./...) ; do
go test -c -tags without_openssl -o /dev/null "$i" > /dev/null
done
}
set -x
set -eux
export GO111MODULE=on
export CGO_ENABLED=0
GOOS=linux GOARCH=amd64 $B
GOOS=linux GOARCH=amd64 build
# See https://github.com/golang/go/wiki/GoArm
GOOS=linux GOARCH=arm GOARM=7 $B
GOOS=linux GOARCH=arm64 $B
GOOS=linux GOARCH=arm GOARM=7 build
GOOS=linux GOARCH=arm64 build
# MacOS on Intel
GOOS=darwin GOARCH=amd64 $B
GOOS=darwin GOARCH=amd64 build
# Catch tests that don't work on MacOS (takes a long time so we only run it once)
time GOOS=darwin GOARCH=amd64 compile_tests
# MacOS on Apple Silicon M1.
# Go 1.16 added support for the M1 and added ios/arm64,
# so we use this to check if we should attempt a build.
if go tool dist list | grep ios/arm64 ; then
GOOS=darwin GOARCH=arm64 $B
GOOS=darwin GOARCH=arm64 build
fi

2
go.mod

@ -3,7 +3,7 @@ module github.com/rfjakob/gocryptfs/v2
go 1.16
require (
github.com/hanwen/go-fuse/v2 v2.1.1-0.20210825171523-3ab5d95a30ae
github.com/hanwen/go-fuse/v2 v2.1.1-0.20211219085202-934a183ed914
github.com/jacobsa/crypto v0.0.0-20190317225127-9f44e2d11115
github.com/jacobsa/oglematchers v0.0.0-20150720000706-141901ea67cd // indirect
github.com/jacobsa/oglemock v0.0.0-20150831005832-e94d794d06ff // indirect

2
go.sum

@ -2,6 +2,8 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/hanwen/go-fuse/v2 v2.1.1-0.20210825171523-3ab5d95a30ae h1:4CB6T4YTUVvnro5ba8ju1QCbOuyGAeF3vvKlo50EJ4k=
github.com/hanwen/go-fuse/v2 v2.1.1-0.20210825171523-3ab5d95a30ae/go.mod h1:B1nGE/6RBFyBRC1RRnf23UpwCdyJ31eukw34oAKukAc=
github.com/hanwen/go-fuse/v2 v2.1.1-0.20211219085202-934a183ed914 h1:hGXMxS1wTE4y+f7iBqFArrJ6X8QozHnEdnVzGZI9Ywc=
github.com/hanwen/go-fuse/v2 v2.1.1-0.20211219085202-934a183ed914/go.mod h1:B1nGE/6RBFyBRC1RRnf23UpwCdyJ31eukw34oAKukAc=
github.com/jacobsa/crypto v0.0.0-20190317225127-9f44e2d11115 h1:YuDUUFNM21CAbyPOpOP8BicaTD/0klJEKt5p8yuw+uY=
github.com/jacobsa/crypto v0.0.0-20190317225127-9f44e2d11115/go.mod h1:LadVJg0XuawGk+8L1rYnIED8451UyNxEMdTWCEt5kmU=
github.com/jacobsa/oglematchers v0.0.0-20150720000706-141901ea67cd h1:9GCSedGjMcLZCrusBZuo4tyKLpKUPenUUqi34AkuFmA=

6
gocryptfs-xray/xray_main.go

@ -148,7 +148,11 @@ func dumpMasterKey(fn string, fido2Path string) {
}
pw = fido2.Secret(fido2Path, cf.FIDO2.CredentialID, cf.FIDO2.HMACSalt)
} else {
pw = readpassword.Once(nil, nil, "")
pw, err = readpassword.Once(nil, nil, "")
if err != nil {
tlog.Fatal.Println(err)
os.Exit(exitcodes.ReadPassword)
}
}
masterkey, err := cf.DecryptMasterKey(pw)
// Purge password from memory

10
init_dir.go

@ -87,7 +87,11 @@ func initDir(args *argContainer) {
password = fido2.Secret(args.fido2, fido2CredentialID, fido2HmacSalt)
} else {
// normal password entry
password = readpassword.Twice([]string(args.extpass), []string(args.passfile))
password, err = readpassword.Twice([]string(args.extpass), []string(args.passfile))
if err != nil {
tlog.Fatal.Println(err)
os.Exit(exitcodes.ReadPassword)
}
fido2CredentialID = nil
fido2HmacSalt = nil
}
@ -102,7 +106,9 @@ func initDir(args *argContainer) {
Fido2CredentialID: fido2CredentialID,
Fido2HmacSalt: fido2HmacSalt,
DeterministicNames: args.deterministic_names,
XChaCha20Poly1305: args.xchacha})
XChaCha20Poly1305: args.xchacha,
LongNameMax: args.longnamemax,
})
if err != nil {
tlog.Fatal.Println(err)
os.Exit(exitcodes.WriteConf)

9
internal/configfile/config_file.go

@ -55,6 +55,8 @@ type ConfFile struct {
FeatureFlags []string
// FIDO2 parameters
FIDO2 *FIDO2Params `json:",omitempty"`
// LongNameMax corresponds to the -longnamemax flag
LongNameMax uint8 `json:",omitempty"`
// Filename is the name of the config file. Not exported to JSON.
filename string
}
@ -71,6 +73,7 @@ type CreateArgs struct {
Fido2HmacSalt []byte
DeterministicNames bool
XChaCha20Poly1305 bool
LongNameMax uint8
}
// Create - create a new config with a random key encrypted with
@ -97,6 +100,12 @@ func Create(args *CreateArgs) error {
if !args.DeterministicNames {
cf.setFeatureFlag(FlagDirIV)
}
// 0 means to *use* the default (which means we don't have to save it), and
// 255 *is* the default, which means we don't have to save it either.
if args.LongNameMax != 0 && args.LongNameMax != 255 {
cf.LongNameMax = args.LongNameMax
cf.setFeatureFlag(FlagLongNameMax)
}
cf.setFeatureFlag(FlagEMENames)
cf.setFeatureFlag(FlagLongNames)
cf.setFeatureFlag(FlagRaw64)

24
internal/configfile/config_test.go

@ -131,6 +131,30 @@ func TestCreateConfFileAESSIV(t *testing.T) {
}
}
func TestCreateConfLongNameMax(t *testing.T) {
args := &CreateArgs{
Filename: "config_test/tmp.conf",
Password: testPw,
LogN: 10,
Creator: "test",
LongNameMax: 100,
}
err := Create(args)
if err != nil {
t.Fatal(err)
}
_, c, err := LoadAndDecrypt("config_test/tmp.conf", testPw)
if err != nil {
t.Fatal(err)
}
if !c.IsFeatureFlagSet(FlagLongNameMax) {
t.Error("FlagLongNameMax should be set but is not")
}
if c.LongNameMax != args.LongNameMax {
t.Errorf("wrong LongNameMax value: want=%d have=%d", args.LongNameMax, c.LongNameMax)
}
}
func TestIsFeatureFlagKnown(t *testing.T) {
// Test a few hardcoded values
testKnownFlags := []string{"DirIV", "PlaintextNames", "EMENames", "GCMIV128", "LongNames", "AESSIV"}

6
internal/configfile/feature_flags.go

@ -14,8 +14,11 @@ const (
// This flag is mandatory since gocryptfs v1.0,
// except when XChaCha20Poly1305 is used.
FlagGCMIV128
// FlagLongNames allows file names longer than 176 bytes.
// FlagLongNames allows file names longer than 175 bytes.
FlagLongNames
// FlagLongNameMax sets a custom name length limit, names longer than that
// will be hashed.
FlagLongNameMax
// FlagAESSIV selects an AES-SIV based crypto backend.
FlagAESSIV
// FlagRaw64 enables raw (unpadded) base64 encoding for file names
@ -40,6 +43,7 @@ var knownFlags = map[flagIota]string{
FlagEMENames: "EMENames",
FlagGCMIV128: "GCMIV128",
FlagLongNames: "LongNames",
FlagLongNameMax: "LongNameMax",
FlagAESSIV: "AESSIV",
FlagRaw64: "Raw64",
FlagHKDF: "HKDF",

37
internal/configfile/validate.go

@ -23,22 +23,23 @@ func (cf *ConfFile) Validate() error {
}
// File content encryption
{
switch {
case cf.IsFeatureFlagSet(FlagXChaCha20Poly1305) && cf.IsFeatureFlagSet(FlagAESSIV):
if cf.IsFeatureFlagSet(FlagXChaCha20Poly1305) && cf.IsFeatureFlagSet(FlagAESSIV) {
return fmt.Errorf("Can't have both XChaCha20Poly1305 and AESSIV feature flags")
case cf.IsFeatureFlagSet(FlagAESSIV):
if !cf.IsFeatureFlagSet(FlagGCMIV128) {
return fmt.Errorf("AESSIV requires GCMIV128 feature flag")
}
case cf.IsFeatureFlagSet(FlagXChaCha20Poly1305):
}
if cf.IsFeatureFlagSet(FlagAESSIV) && !cf.IsFeatureFlagSet(FlagGCMIV128) {
return fmt.Errorf("AESSIV requires GCMIV128 feature flag")
}
if cf.IsFeatureFlagSet(FlagXChaCha20Poly1305) {
if cf.IsFeatureFlagSet(FlagGCMIV128) {
return fmt.Errorf("XChaCha20Poly1305 conflicts with GCMIV128 feature flag")
}
if !cf.IsFeatureFlagSet(FlagHKDF) {
return fmt.Errorf("XChaCha20Poly1305 requires HKDF feature flag")
}
}
// The absence of other flags means AES-GCM (oldest algorithm)
case !cf.IsFeatureFlagSet(FlagXChaCha20Poly1305) && !cf.IsFeatureFlagSet(FlagAESSIV):
if !cf.IsFeatureFlagSet(FlagXChaCha20Poly1305) && !cf.IsFeatureFlagSet(FlagAESSIV) {
if !cf.IsFeatureFlagSet(FlagGCMIV128) {
return fmt.Errorf("AES-GCM requires GCMIV128 feature flag")
}
@ -46,10 +47,10 @@ func (cf *ConfFile) Validate() error {
}
// Filename encryption
{
switch {
case cf.IsFeatureFlagSet(FlagPlaintextNames) && cf.IsFeatureFlagSet(FlagEMENames):
return fmt.Errorf("Can't have both PlaintextNames and EMENames feature flags")
case cf.IsFeatureFlagSet(FlagPlaintextNames):
if cf.IsFeatureFlagSet(FlagPlaintextNames) {
if cf.IsFeatureFlagSet(FlagEMENames) {
return fmt.Errorf("PlaintextNames conflicts with EMENames feature flag")
}
if cf.IsFeatureFlagSet(FlagDirIV) {
return fmt.Errorf("PlaintextNames conflicts with DirIV feature flag")
}
@ -59,9 +60,19 @@ func (cf *ConfFile) Validate() error {
if cf.IsFeatureFlagSet(FlagRaw64) {
return fmt.Errorf("PlaintextNames conflicts with Raw64 feature flag")
}
case cf.IsFeatureFlagSet(FlagEMENames):
if cf.IsFeatureFlagSet(FlagLongNameMax) {
return fmt.Errorf("PlaintextNames conflicts with LongNameMax feature flag")
}
}
if cf.IsFeatureFlagSet(FlagEMENames) {
// All combinations of DirIV, LongNames, Raw64 allowed
}
if cf.LongNameMax != 0 && !cf.IsFeatureFlagSet(FlagLongNameMax) {
return fmt.Errorf("LongNameMax=%d but the LongNameMax feature flag is NOT set", cf.LongNameMax)
}
if cf.LongNameMax == 0 && cf.IsFeatureFlagSet(FlagLongNameMax) {
return fmt.Errorf("LongNameMax=0 but the LongNameMax feature flag IS set")
}
}
return nil
}

29
internal/fusefrontend/node.go

@ -277,7 +277,13 @@ func (n *Node) Mknod(ctx context.Context, name string, mode, rdev uint32, out *f
errno = fs.ToErrno(err)
return
}
inode = n.newChild(ctx, st, out)
if rn.args.ForceOwner != nil {
out.Owner = *rn.args.ForceOwner
}
return inode, 0
}
@ -385,23 +391,17 @@ func (n *Node) Symlink(ctx context.Context, target, name string, out *fuse.Entry
return inode, 0
}
// xfstests generic/013 now also exercises RENAME_EXCHANGE and RENAME_WHITEOUT,
// uncovering lots of problems with longnames
//
// Reject those flags with syscall.EINVAL.
// If we can handle the flags, this function returns 0.
func rejectRenameFlags(flags uint32) syscall.Errno {
// Normal rename, we can handle that
if flags == 0 {
switch flags {
case 0, syscallcompat.RENAME_NOREPLACE, syscallcompat.RENAME_EXCHANGE, syscallcompat.RENAME_WHITEOUT:
return 0
}
// We also can handle RENAME_NOREPLACE
if flags == syscallcompat.RENAME_NOREPLACE {
case syscallcompat.RENAME_NOREPLACE | syscallcompat.RENAME_WHITEOUT:
return 0
default:
tlog.Warn.Printf("rejectRenameFlags: unknown flag combination 0x%x", flags)
return syscall.EINVAL
}
// We cannot handle RENAME_EXCHANGE and RENAME_WHITEOUT yet.
// Needs extra code for .name files.
return syscall.EINVAL
}
// Rename - FUSE call.
@ -466,6 +466,11 @@ func (n *Node) Rename(ctx context.Context, name string, newParent fs.InodeEmbedd
}
return fs.ToErrno(err)
}
if flags&syscallcompat.RENAME_EXCHANGE != 0 || flags&syscallcompat.RENAME_WHITEOUT != 0 {
// These flags mean that there is now a new file at cName and we
// should NOT delete its longname file.
return 0
}
if nametransform.IsLongContent(cName) {
nametransform.DeleteLongNameAt(dirfd, cName)
}

4
internal/fusefrontend/node_xattr.go

@ -15,10 +15,6 @@ import (
// -1 as uint32
const minus1 = ^uint32(0)
// xattr names are encrypted like file names, but with a fixed IV.
// Padded with "_xx" for length 16.
var xattrNameIV = []byte("xattr_name_iv_xx")
// We store encrypted xattrs under this prefix plus the base64-encoded
// encrypted original name.
var xattrStorePrefix = "user.gocryptfs."

12
internal/fusefrontend/node_xattr_darwin.go

@ -27,7 +27,7 @@ func (n *Node) getXAttr(cAttr string) (out []byte, errno syscall.Errno) {
defer syscall.Close(dirfd)
// O_NONBLOCK to not block on FIFOs.
fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_NONBLOCK, 0)
fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0)
if err != nil {
return nil, fs.ToErrno(err)
}
@ -49,10 +49,10 @@ func (n *Node) setXAttr(context *fuse.Context, cAttr string, cData []byte, flags
defer syscall.Close(dirfd)
// O_NONBLOCK to not block on FIFOs.
fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NONBLOCK, 0)
fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0)
// Directories cannot be opened read-write. Retry.
if err == syscall.EISDIR {
fd, err = syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK, 0)
fd, err = syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0)
}
if err != nil {
fs.ToErrno(err)
@ -71,10 +71,10 @@ func (n *Node) removeXAttr(cAttr string) (errno syscall.Errno) {
defer syscall.Close(dirfd)
// O_NONBLOCK to not block on FIFOs.
fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NONBLOCK, 0)
fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0)
// Directories cannot be opened read-write. Retry.
if err == syscall.EISDIR {
fd, err = syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK, 0)
fd, err = syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0)
}
if err != nil {
return fs.ToErrno(err)
@ -93,7 +93,7 @@ func (n *Node) listXAttr() (out []string, errno syscall.Errno) {
defer syscall.Close(dirfd)
// O_NONBLOCK to not block on FIFOs.
fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_NONBLOCK, 0)
fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0)
if err != nil {
return nil, fs.ToErrno(err)
}

4
internal/fusefrontend/root_node.go

@ -268,7 +268,7 @@ func (rn *RootNode) decryptXattrValue(cData []byte) (data []byte, err error) {
// encryptXattrName transforms "user.foo" to "user.gocryptfs.a5sAd4XAa47f5as6dAf"
func (rn *RootNode) encryptXattrName(attr string) (string, error) {
// xattr names are encrypted like file names, but with a fixed IV.
cAttr, err := rn.nameTransform.EncryptName(attr, xattrNameIV)
cAttr, err := rn.nameTransform.EncryptXattrName(attr)
if err != nil {
return "", err
}
@ -282,7 +282,7 @@ func (rn *RootNode) decryptXattrName(cAttr string) (attr string, err error) {
}
// Strip "user.gocryptfs." prefix
cAttr = cAttr[len(xattrStorePrefix):]
attr, err = rn.nameTransform.DecryptName(cAttr, xattrNameIV)
attr, err = rn.nameTransform.DecryptXattrName(cAttr)
if err != nil {
return "", err
}

2
internal/fusefrontend/xattr_unit_test.go

@ -19,7 +19,7 @@ func newTestFS(args Args) *RootNode {
key := make([]byte, cryptocore.KeyLen)
cCore := cryptocore.New(key, cryptocore.BackendGoGCM, contentenc.DefaultIVBits, true)
cEnc := contentenc.New(cCore, contentenc.DefaultBS)
n := nametransform.New(cCore.EMECipher, true, true, nil, false)
n := nametransform.New(cCore.EMECipher, true, 0, true, nil, false)
rn := NewRootNode(args, cEnc, n)
oneSec := time.Second
options := &fs.Options{

2
internal/nametransform/badname.go

@ -48,7 +48,7 @@ func (be *NameTransform) EncryptAndHashBadName(name string, iv []byte, dirfd int
//expand suffix on error
continue
}
if be.longNames && len(cName) > NameMax {
if len(cName) > be.longNameMax {
cNamePart = be.HashLongName(cName)
}
cNameBadReverse := cNamePart + name[charpos:len(name)-len(BadnameSuffix)]

41
internal/nametransform/longnames_test.go

@ -1,7 +1,11 @@
package nametransform
import (
"strings"
"testing"
"github.com/rfjakob/gocryptfs/v2/internal/contentenc"
"github.com/rfjakob/gocryptfs/v2/internal/cryptocore"
)
func TestIsLongName(t *testing.T) {
@ -28,3 +32,40 @@ func TestRemoveLongNameSuffix(t *testing.T) {
t.Error(".name suffix not removed")
}
}
func newLognamesTestInstance(longNameMax uint8) *NameTransform {
key := make([]byte, cryptocore.KeyLen)
cCore := cryptocore.New(key, cryptocore.BackendGoGCM, contentenc.DefaultIVBits, true)
return New(cCore.EMECipher, true, longNameMax, true, nil, false)
}
func TestLongNameMax(t *testing.T) {
iv := make([]byte, 16)
for max := 0; max <= NameMax; max++ {
n := newLognamesTestInstance(uint8(max))
if max == 0 {
// effective value is 255
max = NameMax
}
for l := 0; l <= NameMax+10; l++ {
name := strings.Repeat("x", l)
out, err := n.EncryptAndHashName(name, iv)
if l == 0 || l > NameMax {
if err == nil {
t.Errorf("should have rejected a name of length %d, but did not", l)
}
continue
}
cName, _ := n.EncryptName(name, iv)
rawLen := len(cName)
want := LongNameNone
if rawLen > max {
want = LongNameContent
}
have := NameType(out)
if have != want {
t.Errorf("l=%d max=%d: wanted %v, got %v\nname=%q\nout=%q", l, max, want, have, name, out)
}
}
}
}

62
internal/nametransform/names.go

@ -4,6 +4,7 @@ package nametransform
import (
"crypto/aes"
"encoding/base64"
"math"
"path/filepath"
"syscall"
@ -20,7 +21,9 @@ const (
// NameTransform is used to transform filenames.
type NameTransform struct {
emeCipher *eme.EMECipher
longNames bool
// Names longer than `longNameMax` are hashed. Set to MaxInt when
// longnames are disabled.
longNameMax int
// B64 = either base64.URLEncoding or base64.RawURLEncoding, depending
// on the Raw64 feature flag
B64 *base64.Encoding
@ -30,17 +33,28 @@ type NameTransform struct {
}
// New returns a new NameTransform instance.
func New(e *eme.EMECipher, longNames bool, raw64 bool, badname []string, deterministicNames bool) *NameTransform {
tlog.Debug.Printf("nametransform.New: longNames=%v, raw64=%v, badname=%q",
longNames, raw64, badname)
//
// If `longNames` is set, names longer than `longNameMax` are hashed to
// `gocryptfs.longname.[sha256]`.
// Pass `longNameMax = 0` to use the default value (255).
func New(e *eme.EMECipher, longNames bool, longNameMax uint8, raw64 bool, badname []string, deterministicNames bool) *NameTransform {
tlog.Debug.Printf("nametransform.New: longNameMax=%v, raw64=%v, badname=%q",
longNameMax, raw64, badname)
b64 := base64.URLEncoding
if raw64 {
b64 = base64.RawURLEncoding
}
var effectiveLongNameMax int = math.MaxInt32
if longNames {
if longNameMax == 0 {
effectiveLongNameMax = NameMax
} else {
effectiveLongNameMax = int(longNameMax)
}
}
return &NameTransform{
emeCipher: e,
longNames: longNames,
longNameMax: effectiveLongNameMax,
B64: b64,
badnamePatterns: badname,
deterministicNames: deterministicNames,
@ -52,7 +66,14 @@ func New(e *eme.EMECipher, longNames bool, raw64 bool, badname []string, determi
func (n *NameTransform) DecryptName(cipherName string, iv []byte) (string, error) {
res, err := n.decryptName(cipherName, iv)
if err != nil && n.HaveBadnamePatterns() {
return n.decryptBadname(cipherName, iv)
res, err = n.decryptBadname(cipherName, iv)
}
if err != nil {
return "", err
}
if err := IsValidName(res); err != nil {
tlog.Warn.Printf("DecryptName %q: invalid name after decryption: %v", cipherName, err)
return "", syscall.EBADMSG
}
return res, err
}
@ -65,30 +86,29 @@ func (n *NameTransform) decryptName(cipherName string, iv []byte) (string, error
return "", err
}
if len(bin) == 0 {
tlog.Warn.Printf("DecryptName: empty input")
tlog.Warn.Printf("decryptName: empty input")
return "", syscall.EBADMSG
}
if len(bin)%aes.BlockSize != 0 {
tlog.Debug.Printf("DecryptName %q: decoded length %d is not a multiple of 16", cipherName, len(bin))
tlog.Debug.Printf("decryptName %q: decoded length %d is not a multiple of 16", cipherName, len(bin))
return "", syscall.EBADMSG
}
bin = n.emeCipher.Decrypt(iv, bin)
bin, err = unPad16(bin)
if err != nil {
tlog.Warn.Printf("DecryptName %q: unPad16 error: %v", cipherName, err)
tlog.Warn.Printf("decryptName %q: unPad16 error: %v", cipherName, err)
return "", syscall.EBADMSG
}
plain := string(bin)
if err := IsValidName(plain); err != nil {
tlog.Warn.Printf("DecryptName %q: invalid name after decryption: %v", cipherName, err)
return "", syscall.EBADMSG
}
return plain, err
}
// EncryptName encrypts "plainName", returns a base64-encoded "cipherName64",
// EncryptName encrypts a file name "plainName" and returns a base64-encoded "cipherName64",
// encrypted using EME (https://github.com/rfjakob/eme).
//
// plainName is checked for null bytes, slashes etc. and such names are rejected
// with an error.
//
// This function is exported because in some cases, fusefrontend needs access
// to the full (not hashed) name if longname is used.
func (n *NameTransform) EncryptName(plainName string, iv []byte) (cipherName64 string, err error) {
@ -96,11 +116,19 @@ func (n *NameTransform) EncryptName(plainName string, iv []byte) (cipherName64 s
tlog.Warn.Printf("EncryptName %q: invalid plainName: %v", plainName, err)
return "", syscall.EBADMSG
}
return n.encryptName(plainName, iv), nil
}
// encryptName encrypts "plainName" and returns a base64-encoded "cipherName64",
// encrypted using EME (https://github.com/rfjakob/eme).
//
// No checks for null bytes etc are performed against plainName.
func (n *NameTransform) encryptName(plainName string, iv []byte) (cipherName64 string) {
bin := []byte(plainName)
bin = pad16(bin)
bin = n.emeCipher.Encrypt(iv, bin)
cipherName64 = n.B64.EncodeToString(bin)
return cipherName64, nil
return cipherName64
}
// EncryptAndHashName encrypts "name" and hashes it to a longname if it is
@ -115,7 +143,7 @@ func (be *NameTransform) EncryptAndHashName(name string, iv []byte) (string, err
if err != nil {
return "", err
}
if be.longNames && len(cName) > NameMax {
if len(cName) > be.longNameMax {
return be.HashLongName(cName), nil
}
return cName, nil

23
internal/nametransform/names_test.go

@ -75,3 +75,26 @@ func TestIsValidName(t *testing.T) {
}
}
}
func TestIsValidXattrName(t *testing.T) {
cases := []struct {
in string
want bool
}{
{"", false},
{".", true},
{"..", true},
{"...", true},
{"asdasd/asdasd", true},
{"asdasd\000asdasd", false},
{"hello", true},
{strings.Repeat("x", 255), true},
{strings.Repeat("x", 256), true},
}
for _, c := range cases {
have := isValidXattrName(c.in)
if (have == nil) != c.want {
t.Errorf("isValidXattrName(%q): want %v have %v", c.in, c.want, have)
}
}
}

47
internal/nametransform/xattr.go

@ -0,0 +1,47 @@
package nametransform
import (
"fmt"
"strings"
"syscall"
"github.com/rfjakob/gocryptfs/v2/internal/tlog"
)
// xattr names are encrypted like file names, but with a fixed IV.
// Padded with "_xx" for length 16.
var xattrNameIV = []byte("xattr_name_iv_xx")
func isValidXattrName(name string) error {
if name == "" {
return fmt.Errorf("empty input")
}
if strings.Contains(name, "\000") {
return fmt.Errorf("contains forbidden null byte")
}
return nil
}
// EncryptXattrName encrypts an extended attribute (xattr) name.
// xattr names are encrypted like file names, but with a fixed IV, and fewer
// naming restriction.
func (n *NameTransform) EncryptXattrName(plainName string) (cipherName64 string, err error) {
if err := isValidXattrName(plainName); err != nil {
tlog.Warn.Printf("EncryptXattrName %q: invalid plainName: %v", plainName, err)
return "", syscall.EBADMSG
}
return n.encryptName(plainName, xattrNameIV), nil
}
// DecryptXattrName calls decryptName to try and decrypt a base64-encoded encrypted
// filename "cipherName", and failing that checks if it can be bypassed
func (n *NameTransform) DecryptXattrName(cipherName string) (plainName string, err error) {
if plainName, err = n.decryptName(cipherName, xattrNameIV); err != nil {
return "", err
}
if err := isValidXattrName(plainName); err != nil {
tlog.Warn.Printf("DecryptXattrName %q: invalid name after decryption: %v", cipherName, err)
return "", syscall.EBADMSG
}
return plainName, err
}

73
internal/readpassword/extpass_test.go

@ -2,7 +2,6 @@ package readpassword
import (
"os"
"os/exec"
"testing"
"github.com/rfjakob/gocryptfs/v2/internal/tlog"
@ -16,68 +15,76 @@ func TestMain(m *testing.M) {
func TestExtpass(t *testing.T) {
p1 := "ads2q4tw41reg52"
p2 := string(readPasswordExtpass([]string{"echo " + p1}))
if p1 != p2 {
t.Errorf("p1=%q != p2=%q", p1, p2)
p2, err := readPasswordExtpass([]string{"echo " + p1})
if err != nil {
t.Fatal(err)
}
if p1 != string(p2) {
t.Errorf("p1=%q != p2=%q", p1, string(p2))
}
}
func TestOnceExtpass(t *testing.T) {
p1 := "lkadsf0923rdfi48rqwhdsf"
p2 := string(Once([]string{"echo " + p1}, nil, ""))
if p1 != p2 {
t.Errorf("p1=%q != p2=%q", p1, p2)
p2, err := Once([]string{"echo " + p1}, nil, "")
if err != nil {
t.Fatal(err)
}
if p1 != string(p2) {
t.Errorf("p1=%q != p2=%q", p1, string(p2))
}
}
// extpass with two arguments
func TestOnceExtpass2(t *testing.T) {
p1 := "foo"
p2 := string(Once([]string{"echo", p1}, nil, ""))
if p1 != p2 {
t.Errorf("p1=%q != p2=%q", p1, p2)
p2, err := Once([]string{"echo", p1}, nil, "")
if err != nil {
t.Fatal(err)
}
if p1 != string(p2) {
t.Errorf("p1=%q != p2=%q", p1, string(p2))
}
}
// extpass with three arguments
func TestOnceExtpass3(t *testing.T) {
p1 := "foo bar baz"
p2 := string(Once([]string{"echo", "foo", "bar", "baz"}, nil, ""))
if p1 != p2 {
t.Errorf("p1=%q != p2=%q", p1, p2)
p2, err := Once([]string{"echo", "foo", "bar", "baz"}, nil, "")
if err != nil {
t.Fatal(err)
}
if p1 != string(p2) {
t.Errorf("p1=%q != p2=%q", p1, string(p2))
}
}
func TestOnceExtpassSpaces(t *testing.T) {
p1 := "mypassword"
p2 := string(Once([]string{"cat", "passfile_test_files/file with spaces.txt"}, nil, ""))
if p1 != p2 {
t.Errorf("p1=%q != p2=%q", p1, p2)
p2, err := Once([]string{"cat", "passfile_test_files/file with spaces.txt"}, nil, "")
if err != nil {
t.Fatal(err)
}
if p1 != string(p2) {
t.Errorf("p1=%q != p2=%q", p1, string(p2))
}
}
func TestTwiceExtpass(t *testing.T) {
p1 := "w5w44t3wfe45srz434"
p2 := string(Once([]string{"echo " + p1}, nil, ""))
if p1 != p2 {
t.Errorf("p1=%q != p2=%q", p1, p2)
p2, err := Once([]string{"echo " + p1}, nil, "")
if err != nil {
t.Fatal(err)
}
if p1 != string(p2) {
t.Errorf("p1=%q != p2=%q", p1, string(p2))
}
}
// When extpass returns an empty string, we should crash.
//
// The TEST_SLAVE magic is explained at
// https://talks.golang.org/2014/testing.slide#23 .
// Empty extpass should fail
func TestExtpassEmpty(t *testing.T) {
if os.Getenv("TEST_SLAVE") == "1" {
readPasswordExtpass([]string{"echo"})
return
}
cmd := exec.Command(os.Args[0], "-test.run=TestExtpassEmpty$")
cmd.Env = append(os.Environ(), "TEST_SLAVE=1")
err := cmd.Run()
if err != nil {
return
_, err := readPasswordExtpass([]string{"echo"})
if err == nil {
t.Fatal("empty password should have failed")
}
t.Fatal("empty password should have failed")
}

28
internal/readpassword/passfile.go

@ -2,28 +2,31 @@ package readpassword
import (
"bytes"
"fmt"
"os"
"github.com/rfjakob/gocryptfs/v2/internal/exitcodes"
"github.com/rfjakob/gocryptfs/v2/internal/tlog"
)
// readPassFileConcatenate reads the first line from each file name and
// concatenates the results. The result does not contain any newlines.
func readPassFileConcatenate(passfileSlice []string) (result []byte) {
func readPassFileConcatenate(passfileSlice []string) (result []byte, err error) {
for _, e := range passfileSlice {
result = append(result, readPassFile(e)...)
add, err := readPassFile(e)
if err != nil {
return nil, err
}
result = append(result, add...)
}
return result
return result, nil
}
// readPassFile reads the first line from the passed file name.
func readPassFile(passfile string) []byte {
func readPassFile(passfile string) ([]byte, error) {
tlog.Info.Printf("passfile: reading from file %q", passfile)
f, err := os.Open(passfile)
if err != nil {
tlog.Fatal.Printf("fatal: passfile: could not open %q: %v", passfile, err)
os.Exit(exitcodes.ReadPassword)
return nil, fmt.Errorf("fatal: passfile: could not open %q: %v", passfile, err)
}
defer f.Close()
// +1 for an optional trailing newline,
@ -31,23 +34,20 @@ func readPassFile(passfile string) []byte {
buf := make([]byte, maxPasswordLen+2)
n, err := f.Read(buf)
if err != nil {
tlog.Fatal.Printf("fatal: passfile: could not read from %q: %v", passfile, err)
os.Exit(exitcodes.ReadPassword)
return nil, fmt.Errorf("fatal: passfile: could not read from %q: %v", passfile, err)
}
buf = buf[:n]
// Split into first line and "trailing garbage"
lines := bytes.SplitN(buf, []byte("\n"), 2)
if len(lines[0]) == 0 {
tlog.Fatal.Printf("fatal: passfile: empty first line in %q", passfile)
os.Exit(exitcodes.ReadPassword)
return nil, fmt.Errorf("fatal: passfile: empty first line in %q", passfile)
}
if len(lines[0]) > maxPasswordLen {
tlog.Fatal.Printf("fatal: passfile: max password length (%d bytes) exceeded", maxPasswordLen)
os.Exit(exitcodes.ReadPassword)
return nil, fmt.Errorf("fatal: passfile: max password length (%d bytes) exceeded", maxPasswordLen)
}
if len(lines) > 1 && len(lines[1]) > 0 {
tlog.Warn.Printf("warning: passfile: ignoring trailing garbage (%d bytes) after first line",
len(lines[1]))
}
return lines[0]
return lines[0], nil
}

76
internal/readpassword/passfile_test.go

@ -1,8 +1,6 @@
package readpassword
import (
"os"
"os/exec"
"testing"
)
@ -17,76 +15,49 @@ func TestPassfile(t *testing.T) {
{"file with spaces.txt", "mypassword"},
}
for _, tc := range testcases {
pw := readPassFile("passfile_test_files/" + tc.file)
pw, err := readPassFile("passfile_test_files/" + tc.file)
if err != nil {
t.Fatal(err)
}
if string(pw) != tc.want {
t.Errorf("Wrong result: want=%q have=%q", tc.want, pw)
}
// Calling readPassFileConcatenate with only one element should give the
// same result
pw = readPassFileConcatenate([]string{"passfile_test_files/" + tc.file})
pw, err = readPassFileConcatenate([]string{"passfile_test_files/" + tc.file})
if err != nil {
t.Fatal(err)
}
if string(pw) != tc.want {
t.Errorf("Wrong result: want=%q have=%q", tc.want, pw)
}
}
}
// readPassFile() should exit instead of returning an empty string.
//
// The TEST_SLAVE magic is explained at
// https://talks.golang.org/2014/testing.slide#23 , mirror:
// http://web.archive.org/web/20200426174352/https://talks.golang.org/2014/testing.slide#23
// readPassFile() should fail instead of returning an empty string.
func TestPassfileEmpty(t *testing.T) {