Compare commits
17 Commits
eb42e54182
...
a2b54cfccd
Author | SHA1 | Date |
---|---|---|
Jakob Unterwurzacher | a2b54cfccd | |
Jakob Unterwurzacher | ba75aa1ab0 | |
Jakob Unterwurzacher | b636f79f89 | |
Jakob Unterwurzacher | 47358938ec | |
Jakob Unterwurzacher | 696f11499b | |
Jakob Unterwurzacher | b859bc96ef | |
Jakob Unterwurzacher | 3bac814ea9 | |
Jakob Unterwurzacher | b7cac4ffd0 | |
Jakob Unterwurzacher | 3ca2b1983d | |
Jakob Unterwurzacher | 5f955423b7 | |
Jakob Unterwurzacher | c23a7f2259 | |
Jakob Unterwurzacher | 700ae685cc | |
Jakob Unterwurzacher | 4b251f3ce1 | |
Jakob Unterwurzacher | 1eaf1211a2 | |
Jakob Unterwurzacher | 5749e70c7c | |
Jakob Unterwurzacher | 8d8b76dcac | |
Jakob Unterwurzacher | 64be5de75f |
|
@ -177,6 +177,10 @@ MOUNT OPTIONS
|
||||||
Available options for mounting are listed below. Usually, you don't need any.
|
Available options for mounting are listed below. Usually, you don't need any.
|
||||||
Defaults are fine.
|
Defaults are fine.
|
||||||
|
|
||||||
|
#### -acl
|
||||||
|
Enable ACL enforcement. When you want to use ACLs, you must enable this
|
||||||
|
option.
|
||||||
|
|
||||||
#### -allow_other
|
#### -allow_other
|
||||||
By default, the Linux kernel prevents any other user (even root) to
|
By default, the Linux kernel prevents any other user (even root) to
|
||||||
access a mounted FUSE filesystem. Settings this option allows access for
|
access a mounted FUSE filesystem. Settings this option allows access for
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -3,7 +3,7 @@ module github.com/rfjakob/gocryptfs/v2
|
||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
require (
|
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/crypto v0.0.0-20190317225127-9f44e2d11115
|
||||||
github.com/jacobsa/oglematchers v0.0.0-20150720000706-141901ea67cd // indirect
|
github.com/jacobsa/oglematchers v0.0.0-20150720000706-141901ea67cd // indirect
|
||||||
github.com/jacobsa/oglemock v0.0.0-20150831005832-e94d794d06ff // indirect
|
github.com/jacobsa/oglemock v0.0.0-20150831005832-e94d794d06ff // indirect
|
||||||
|
|
2
go.sum
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/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 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.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 h1:YuDUUFNM21CAbyPOpOP8BicaTD/0klJEKt5p8yuw+uY=
|
||||||
github.com/jacobsa/crypto v0.0.0-20190317225127-9f44e2d11115/go.mod h1:LadVJg0XuawGk+8L1rYnIED8451UyNxEMdTWCEt5kmU=
|
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=
|
github.com/jacobsa/oglematchers v0.0.0-20150720000706-141901ea67cd h1:9GCSedGjMcLZCrusBZuo4tyKLpKUPenUUqi34AkuFmA=
|
||||||
|
|
|
@ -148,7 +148,11 @@ func dumpMasterKey(fn string, fido2Path string) {
|
||||||
}
|
}
|
||||||
pw = fido2.Secret(fido2Path, cf.FIDO2.CredentialID, cf.FIDO2.HMACSalt)
|
pw = fido2.Secret(fido2Path, cf.FIDO2.CredentialID, cf.FIDO2.HMACSalt)
|
||||||
} else {
|
} 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)
|
masterkey, err := cf.DecryptMasterKey(pw)
|
||||||
// Purge password from memory
|
// Purge password from memory
|
||||||
|
|
|
@ -87,7 +87,11 @@ func initDir(args *argContainer) {
|
||||||
password = fido2.Secret(args.fido2, fido2CredentialID, fido2HmacSalt)
|
password = fido2.Secret(args.fido2, fido2CredentialID, fido2HmacSalt)
|
||||||
} else {
|
} else {
|
||||||
// normal password entry
|
// 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
|
fido2CredentialID = nil
|
||||||
fido2HmacSalt = nil
|
fido2HmacSalt = nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -277,7 +277,13 @@ func (n *Node) Mknod(ctx context.Context, name string, mode, rdev uint32, out *f
|
||||||
errno = fs.ToErrno(err)
|
errno = fs.ToErrno(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
inode = n.newChild(ctx, st, out)
|
inode = n.newChild(ctx, st, out)
|
||||||
|
|
||||||
|
if rn.args.ForceOwner != nil {
|
||||||
|
out.Owner = *rn.args.ForceOwner
|
||||||
|
}
|
||||||
|
|
||||||
return inode, 0
|
return inode, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -385,23 +391,17 @@ func (n *Node) Symlink(ctx context.Context, target, name string, out *fuse.Entry
|
||||||
return inode, 0
|
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.
|
// If we can handle the flags, this function returns 0.
|
||||||
func rejectRenameFlags(flags uint32) syscall.Errno {
|
func rejectRenameFlags(flags uint32) syscall.Errno {
|
||||||
// Normal rename, we can handle that
|
switch flags {
|
||||||
if flags == 0 {
|
case 0, syscallcompat.RENAME_NOREPLACE, syscallcompat.RENAME_EXCHANGE, syscallcompat.RENAME_WHITEOUT:
|
||||||
return 0
|
return 0
|
||||||
}
|
case syscallcompat.RENAME_NOREPLACE | syscallcompat.RENAME_WHITEOUT:
|
||||||
// We also can handle RENAME_NOREPLACE
|
|
||||||
if flags == syscallcompat.RENAME_NOREPLACE {
|
|
||||||
return 0
|
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.
|
// Rename - FUSE call.
|
||||||
|
@ -466,6 +466,11 @@ func (n *Node) Rename(ctx context.Context, name string, newParent fs.InodeEmbedd
|
||||||
}
|
}
|
||||||
return fs.ToErrno(err)
|
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) {
|
if nametransform.IsLongContent(cName) {
|
||||||
nametransform.DeleteLongNameAt(dirfd, cName)
|
nametransform.DeleteLongNameAt(dirfd, cName)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,10 +15,6 @@ import (
|
||||||
// -1 as uint32
|
// -1 as uint32
|
||||||
const minus1 = ^uint32(0)
|
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
|
// We store encrypted xattrs under this prefix plus the base64-encoded
|
||||||
// encrypted original name.
|
// encrypted original name.
|
||||||
var xattrStorePrefix = "user.gocryptfs."
|
var xattrStorePrefix = "user.gocryptfs."
|
||||||
|
|
|
@ -268,7 +268,7 @@ func (rn *RootNode) decryptXattrValue(cData []byte) (data []byte, err error) {
|
||||||
// encryptXattrName transforms "user.foo" to "user.gocryptfs.a5sAd4XAa47f5as6dAf"
|
// encryptXattrName transforms "user.foo" to "user.gocryptfs.a5sAd4XAa47f5as6dAf"
|
||||||
func (rn *RootNode) encryptXattrName(attr string) (string, error) {
|
func (rn *RootNode) encryptXattrName(attr string) (string, error) {
|
||||||
// xattr names are encrypted like file names, but with a fixed IV.
|
// 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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -282,7 +282,7 @@ func (rn *RootNode) decryptXattrName(cAttr string) (attr string, err error) {
|
||||||
}
|
}
|
||||||
// Strip "user.gocryptfs." prefix
|
// Strip "user.gocryptfs." prefix
|
||||||
cAttr = cAttr[len(xattrStorePrefix):]
|
cAttr = cAttr[len(xattrStorePrefix):]
|
||||||
attr, err = rn.nameTransform.DecryptName(cAttr, xattrNameIV)
|
attr, err = rn.nameTransform.DecryptXattrName(cAttr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,7 +66,14 @@ func New(e *eme.EMECipher, longNames bool, longNameMax uint8, raw64 bool, badnam
|
||||||
func (n *NameTransform) DecryptName(cipherName string, iv []byte) (string, error) {
|
func (n *NameTransform) DecryptName(cipherName string, iv []byte) (string, error) {
|
||||||
res, err := n.decryptName(cipherName, iv)
|
res, err := n.decryptName(cipherName, iv)
|
||||||
if err != nil && n.HaveBadnamePatterns() {
|
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
|
return res, err
|
||||||
}
|
}
|
||||||
|
@ -79,30 +86,29 @@ func (n *NameTransform) decryptName(cipherName string, iv []byte) (string, error
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if len(bin) == 0 {
|
if len(bin) == 0 {
|
||||||
tlog.Warn.Printf("DecryptName: empty input")
|
tlog.Warn.Printf("decryptName: empty input")
|
||||||
return "", syscall.EBADMSG
|
return "", syscall.EBADMSG
|
||||||
}
|
}
|
||||||
if len(bin)%aes.BlockSize != 0 {
|
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
|
return "", syscall.EBADMSG
|
||||||
}
|
}
|
||||||
bin = n.emeCipher.Decrypt(iv, bin)
|
bin = n.emeCipher.Decrypt(iv, bin)
|
||||||
bin, err = unPad16(bin)
|
bin, err = unPad16(bin)
|
||||||
if err != nil {
|
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
|
return "", syscall.EBADMSG
|
||||||
}
|
}
|
||||||
plain := string(bin)
|
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
|
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).
|
// 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
|
// This function is exported because in some cases, fusefrontend needs access
|
||||||
// to the full (not hashed) name if longname is used.
|
// to the full (not hashed) name if longname is used.
|
||||||
func (n *NameTransform) EncryptName(plainName string, iv []byte) (cipherName64 string, err error) {
|
func (n *NameTransform) EncryptName(plainName string, iv []byte) (cipherName64 string, err error) {
|
||||||
|
@ -110,11 +116,19 @@ func (n *NameTransform) EncryptName(plainName string, iv []byte) (cipherName64 s
|
||||||
tlog.Warn.Printf("EncryptName %q: invalid plainName: %v", plainName, err)
|
tlog.Warn.Printf("EncryptName %q: invalid plainName: %v", plainName, err)
|
||||||
return "", syscall.EBADMSG
|
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 := []byte(plainName)
|
||||||
bin = pad16(bin)
|
bin = pad16(bin)
|
||||||
bin = n.emeCipher.Encrypt(iv, bin)
|
bin = n.emeCipher.Encrypt(iv, bin)
|
||||||
cipherName64 = n.B64.EncodeToString(bin)
|
cipherName64 = n.B64.EncodeToString(bin)
|
||||||
return cipherName64, nil
|
return cipherName64
|
||||||
}
|
}
|
||||||
|
|
||||||
// EncryptAndHashName encrypts "name" and hashes it to a longname if it is
|
// EncryptAndHashName encrypts "name" and hashes it to a longname if it is
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -2,7 +2,6 @@ package readpassword
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/rfjakob/gocryptfs/v2/internal/tlog"
|
"github.com/rfjakob/gocryptfs/v2/internal/tlog"
|
||||||
|
@ -16,68 +15,76 @@ func TestMain(m *testing.M) {
|
||||||
|
|
||||||
func TestExtpass(t *testing.T) {
|
func TestExtpass(t *testing.T) {
|
||||||
p1 := "ads2q4tw41reg52"
|
p1 := "ads2q4tw41reg52"
|
||||||
p2 := string(readPasswordExtpass([]string{"echo " + p1}))
|
p2, err := readPasswordExtpass([]string{"echo " + p1})
|
||||||
if p1 != p2 {
|
if err != nil {
|
||||||
t.Errorf("p1=%q != p2=%q", p1, p2)
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if p1 != string(p2) {
|
||||||
|
t.Errorf("p1=%q != p2=%q", p1, string(p2))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOnceExtpass(t *testing.T) {
|
func TestOnceExtpass(t *testing.T) {
|
||||||
p1 := "lkadsf0923rdfi48rqwhdsf"
|
p1 := "lkadsf0923rdfi48rqwhdsf"
|
||||||
p2 := string(Once([]string{"echo " + p1}, nil, ""))
|
p2, err := Once([]string{"echo " + p1}, nil, "")
|
||||||
if p1 != p2 {
|
if err != nil {
|
||||||
t.Errorf("p1=%q != p2=%q", p1, p2)
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if p1 != string(p2) {
|
||||||
|
t.Errorf("p1=%q != p2=%q", p1, string(p2))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// extpass with two arguments
|
// extpass with two arguments
|
||||||
func TestOnceExtpass2(t *testing.T) {
|
func TestOnceExtpass2(t *testing.T) {
|
||||||
p1 := "foo"
|
p1 := "foo"
|
||||||
p2 := string(Once([]string{"echo", p1}, nil, ""))
|
p2, err := Once([]string{"echo", p1}, nil, "")
|
||||||
if p1 != p2 {
|
if err != nil {
|
||||||
t.Errorf("p1=%q != p2=%q", p1, p2)
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if p1 != string(p2) {
|
||||||
|
t.Errorf("p1=%q != p2=%q", p1, string(p2))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// extpass with three arguments
|
// extpass with three arguments
|
||||||
func TestOnceExtpass3(t *testing.T) {
|
func TestOnceExtpass3(t *testing.T) {
|
||||||
p1 := "foo bar baz"
|
p1 := "foo bar baz"
|
||||||
p2 := string(Once([]string{"echo", "foo", "bar", "baz"}, nil, ""))
|
p2, err := Once([]string{"echo", "foo", "bar", "baz"}, nil, "")
|
||||||
if p1 != p2 {
|
if err != nil {
|
||||||
t.Errorf("p1=%q != p2=%q", p1, p2)
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if p1 != string(p2) {
|
||||||
|
t.Errorf("p1=%q != p2=%q", p1, string(p2))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOnceExtpassSpaces(t *testing.T) {
|
func TestOnceExtpassSpaces(t *testing.T) {
|
||||||
p1 := "mypassword"
|
p1 := "mypassword"
|
||||||
p2 := string(Once([]string{"cat", "passfile_test_files/file with spaces.txt"}, nil, ""))
|
p2, err := Once([]string{"cat", "passfile_test_files/file with spaces.txt"}, nil, "")
|
||||||
if p1 != p2 {
|
if err != nil {
|
||||||
t.Errorf("p1=%q != p2=%q", p1, p2)
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if p1 != string(p2) {
|
||||||
|
t.Errorf("p1=%q != p2=%q", p1, string(p2))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTwiceExtpass(t *testing.T) {
|
func TestTwiceExtpass(t *testing.T) {
|
||||||
p1 := "w5w44t3wfe45srz434"
|
p1 := "w5w44t3wfe45srz434"
|
||||||
p2 := string(Once([]string{"echo " + p1}, nil, ""))
|
p2, err := Once([]string{"echo " + p1}, nil, "")
|
||||||
if p1 != p2 {
|
if err != nil {
|
||||||
t.Errorf("p1=%q != p2=%q", p1, p2)
|
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.
|
// Empty extpass should fail
|
||||||
//
|
|
||||||
// The TEST_SLAVE magic is explained at
|
|
||||||
// https://talks.golang.org/2014/testing.slide#23 .
|
|
||||||
func TestExtpassEmpty(t *testing.T) {
|
func TestExtpassEmpty(t *testing.T) {
|
||||||
if os.Getenv("TEST_SLAVE") == "1" {
|
_, err := readPasswordExtpass([]string{"echo"})
|
||||||
readPasswordExtpass([]string{"echo"})
|
if err == nil {
|
||||||
return
|
t.Fatal("empty password should have failed")
|
||||||
}
|
}
|
||||||
cmd := exec.Command(os.Args[0], "-test.run=TestExtpassEmpty$")
|
|
||||||
cmd.Env = append(os.Environ(), "TEST_SLAVE=1")
|
|
||||||
err := cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.Fatal("empty password should have failed")
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,28 +2,31 @@ package readpassword
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/rfjakob/gocryptfs/v2/internal/exitcodes"
|
|
||||||
"github.com/rfjakob/gocryptfs/v2/internal/tlog"
|
"github.com/rfjakob/gocryptfs/v2/internal/tlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// readPassFileConcatenate reads the first line from each file name and
|
// readPassFileConcatenate reads the first line from each file name and
|
||||||
// concatenates the results. The result does not contain any newlines.
|
// 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 {
|
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.
|
// 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)
|
tlog.Info.Printf("passfile: reading from file %q", passfile)
|
||||||
f, err := os.Open(passfile)
|
f, err := os.Open(passfile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.Fatal.Printf("fatal: passfile: could not open %q: %v", passfile, err)
|
return nil, fmt.Errorf("fatal: passfile: could not open %q: %v", passfile, err)
|
||||||
os.Exit(exitcodes.ReadPassword)
|
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
// +1 for an optional trailing newline,
|
// +1 for an optional trailing newline,
|
||||||
|
@ -31,23 +34,20 @@ func readPassFile(passfile string) []byte {
|
||||||
buf := make([]byte, maxPasswordLen+2)
|
buf := make([]byte, maxPasswordLen+2)
|
||||||
n, err := f.Read(buf)
|
n, err := f.Read(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.Fatal.Printf("fatal: passfile: could not read from %q: %v", passfile, err)
|
return nil, fmt.Errorf("fatal: passfile: could not read from %q: %v", passfile, err)
|
||||||
os.Exit(exitcodes.ReadPassword)
|
|
||||||
}
|
}
|
||||||
buf = buf[:n]
|
buf = buf[:n]
|
||||||
// Split into first line and "trailing garbage"
|
// Split into first line and "trailing garbage"
|
||||||
lines := bytes.SplitN(buf, []byte("\n"), 2)
|
lines := bytes.SplitN(buf, []byte("\n"), 2)
|
||||||
if len(lines[0]) == 0 {
|
if len(lines[0]) == 0 {
|
||||||
tlog.Fatal.Printf("fatal: passfile: empty first line in %q", passfile)
|
return nil, fmt.Errorf("fatal: passfile: empty first line in %q", passfile)
|
||||||
os.Exit(exitcodes.ReadPassword)
|
|
||||||
}
|
}
|
||||||
if len(lines[0]) > maxPasswordLen {
|
if len(lines[0]) > maxPasswordLen {
|
||||||
tlog.Fatal.Printf("fatal: passfile: max password length (%d bytes) exceeded", maxPasswordLen)
|
return nil, fmt.Errorf("fatal: passfile: max password length (%d bytes) exceeded", maxPasswordLen)
|
||||||
os.Exit(exitcodes.ReadPassword)
|
|
||||||
}
|
}
|
||||||
if len(lines) > 1 && len(lines[1]) > 0 {
|
if len(lines) > 1 && len(lines[1]) > 0 {
|
||||||
tlog.Warn.Printf("warning: passfile: ignoring trailing garbage (%d bytes) after first line",
|
tlog.Warn.Printf("warning: passfile: ignoring trailing garbage (%d bytes) after first line",
|
||||||
len(lines[1]))
|
len(lines[1]))
|
||||||
}
|
}
|
||||||
return lines[0]
|
return lines[0], nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package readpassword
|
package readpassword
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,76 +15,49 @@ func TestPassfile(t *testing.T) {
|
||||||
{"file with spaces.txt", "mypassword"},
|
{"file with spaces.txt", "mypassword"},
|
||||||
}
|
}
|
||||||
for _, tc := range testcases {
|
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 {
|
if string(pw) != tc.want {
|
||||||
t.Errorf("Wrong result: want=%q have=%q", tc.want, pw)
|
t.Errorf("Wrong result: want=%q have=%q", tc.want, pw)
|
||||||
}
|
}
|
||||||
// Calling readPassFileConcatenate with only one element should give the
|
// Calling readPassFileConcatenate with only one element should give the
|
||||||
// same result
|
// 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 {
|
if string(pw) != tc.want {
|
||||||
t.Errorf("Wrong result: want=%q have=%q", tc.want, pw)
|
t.Errorf("Wrong result: want=%q have=%q", tc.want, pw)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// readPassFile() should exit instead of returning an empty string.
|
// readPassFile() should fail 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
|
|
||||||
func TestPassfileEmpty(t *testing.T) {
|
func TestPassfileEmpty(t *testing.T) {
|
||||||
if os.Getenv("TEST_SLAVE") == "1" {
|
_, err := readPassFile("passfile_test_files/empty.txt")
|
||||||
readPassFile("passfile_test_files/empty.txt")
|
if err == nil {
|
||||||
return
|
t.Fatal("should have failed")
|
||||||
}
|
}
|
||||||
cmd := exec.Command(os.Args[0], "-test.run=TestPassfileEmpty$")
|
|
||||||
cmd.Env = append(os.Environ(), "TEST_SLAVE=1")
|
|
||||||
err := cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.Fatal("should have exited")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// File containing just a newline.
|
// File containing just a newline.
|
||||||
// readPassFile() should exit instead of returning an empty string.
|
// readPassFile() should fal 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
|
|
||||||
func TestPassfileNewline(t *testing.T) {
|
func TestPassfileNewline(t *testing.T) {
|
||||||
if os.Getenv("TEST_SLAVE") == "1" {
|
_, err := readPassFile("passfile_test_files/newline.txt")
|
||||||
readPassFile("passfile_test_files/newline.txt")
|
if err == nil {
|
||||||
return
|
t.Fatal("should have failed")
|
||||||
}
|
}
|
||||||
cmd := exec.Command(os.Args[0], "-test.run=TestPassfileNewline$")
|
|
||||||
cmd.Env = append(os.Environ(), "TEST_SLAVE=1")
|
|
||||||
err := cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.Fatal("should have exited")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// File containing "\ngarbage".
|
// File containing "\ngarbage".
|
||||||
// readPassFile() should exit instead of returning an empty string.
|
// readPassFile() should return an error.
|
||||||
//
|
|
||||||
// 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
|
|
||||||
func TestPassfileEmptyFirstLine(t *testing.T) {
|
func TestPassfileEmptyFirstLine(t *testing.T) {
|
||||||
if os.Getenv("TEST_SLAVE") == "1" {
|
_, err := readPassFile("passfile_test_files/empty_first_line.txt")
|
||||||
readPassFile("passfile_test_files/empty_first_line.txt")
|
if err == nil {
|
||||||
return
|
t.Fatal("should have failed")
|
||||||
}
|
}
|
||||||
cmd := exec.Command(os.Args[0], "-test.run=TestPassfileEmptyFirstLine$")
|
|
||||||
cmd.Env = append(os.Environ(), "TEST_SLAVE=1")
|
|
||||||
err := cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.Fatal("should have exited")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestPassFileConcatenate tests readPassFileConcatenate
|
// TestPassFileConcatenate tests readPassFileConcatenate
|
||||||
|
@ -95,8 +66,11 @@ func TestPassFileConcatenate(t *testing.T) {
|
||||||
"passfile_test_files/file with spaces.txt",
|
"passfile_test_files/file with spaces.txt",
|
||||||
"passfile_test_files/mypassword_garbage.txt",
|
"passfile_test_files/mypassword_garbage.txt",
|
||||||
}
|
}
|
||||||
res := string(readPassFileConcatenate(files))
|
res, err := readPassFileConcatenate(files)
|
||||||
if res != "mypasswordmypassword" {
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if string(res) != "mypasswordmypassword" {
|
||||||
t.Errorf("wrong result: %q", res)
|
t.Errorf("wrong result: %q", res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ import (
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh/terminal"
|
"golang.org/x/crypto/ssh/terminal"
|
||||||
|
|
||||||
"github.com/rfjakob/gocryptfs/v2/internal/exitcodes"
|
|
||||||
"github.com/rfjakob/gocryptfs/v2/internal/tlog"
|
"github.com/rfjakob/gocryptfs/v2/internal/tlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,7 +21,7 @@ const (
|
||||||
|
|
||||||
// Once tries to get a password from the user, either from the terminal, extpass, passfile
|
// Once tries to get a password from the user, either from the terminal, extpass, passfile
|
||||||
// or stdin. Leave "prompt" empty to use the default "Password: " prompt.
|
// or stdin. Leave "prompt" empty to use the default "Password: " prompt.
|
||||||
func Once(extpass []string, passfile []string, prompt string) []byte {
|
func Once(extpass []string, passfile []string, prompt string) ([]byte, error) {
|
||||||
if len(passfile) != 0 {
|
if len(passfile) != 0 {
|
||||||
return readPassFileConcatenate(passfile)
|
return readPassFileConcatenate(passfile)
|
||||||
}
|
}
|
||||||
|
@ -40,7 +39,7 @@ func Once(extpass []string, passfile []string, prompt string) []byte {
|
||||||
|
|
||||||
// Twice is the same as Once but will prompt twice if we get the password from
|
// Twice is the same as Once but will prompt twice if we get the password from
|
||||||
// the terminal.
|
// the terminal.
|
||||||
func Twice(extpass []string, passfile []string) []byte {
|
func Twice(extpass []string, passfile []string) ([]byte, error) {
|
||||||
if len(passfile) != 0 {
|
if len(passfile) != 0 {
|
||||||
return readPassFileConcatenate(passfile)
|
return readPassFileConcatenate(passfile)
|
||||||
}
|
}
|
||||||
|
@ -50,54 +49,59 @@ func Twice(extpass []string, passfile []string) []byte {
|
||||||
if !terminal.IsTerminal(int(os.Stdin.Fd())) {
|
if !terminal.IsTerminal(int(os.Stdin.Fd())) {
|
||||||
return readPasswordStdin("Password")
|
return readPasswordStdin("Password")
|
||||||
}
|
}
|
||||||
p1 := readPasswordTerminal("Password: ")
|
p1, err := readPasswordTerminal("Password: ")
|
||||||
p2 := readPasswordTerminal("Repeat: ")
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p2, err := readPasswordTerminal("Repeat: ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if !bytes.Equal(p1, p2) {
|
if !bytes.Equal(p1, p2) {
|
||||||
tlog.Fatal.Println("Passwords do not match")
|
return nil, fmt.Errorf("Passwords do not match")
|
||||||
os.Exit(exitcodes.ReadPassword)
|
|
||||||
}
|
}
|
||||||
// Wipe the password duplicate from memory
|
// Wipe the password duplicate from memory
|
||||||
for i := range p2 {
|
for i := range p2 {
|
||||||
p2[i] = 0
|
p2[i] = 0
|
||||||
}
|
}
|
||||||
return p1
|
return p1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// readPasswordTerminal reads a line from the terminal.
|
// readPasswordTerminal reads a line from the terminal.
|
||||||
// Exits on read error or empty result.
|
// Exits on read error or empty result.
|
||||||
func readPasswordTerminal(prompt string) []byte {
|
func readPasswordTerminal(prompt string) ([]byte, error) {
|
||||||
fd := int(os.Stdin.Fd())
|
fd := int(os.Stdin.Fd())
|
||||||
fmt.Fprintf(os.Stderr, prompt)
|
fmt.Fprintf(os.Stderr, prompt)
|
||||||
// terminal.ReadPassword removes the trailing newline
|
// terminal.ReadPassword removes the trailing newline
|
||||||
p, err := terminal.ReadPassword(fd)
|
p, err := terminal.ReadPassword(fd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.Fatal.Printf("Could not read password from terminal: %v\n", err)
|
return nil, fmt.Errorf("Could not read password from terminal: %v\n", err)
|
||||||
os.Exit(exitcodes.ReadPassword)
|
|
||||||
}
|
}
|
||||||
fmt.Fprintf(os.Stderr, "\n")
|
fmt.Fprintf(os.Stderr, "\n")
|
||||||
if len(p) == 0 {
|
if len(p) == 0 {
|
||||||
tlog.Fatal.Println("Password is empty")
|
return nil, fmt.Errorf("Password is empty")
|
||||||
os.Exit(exitcodes.PasswordEmpty)
|
|
||||||
}
|
}
|
||||||
return p
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// readPasswordStdin reads a line from stdin.
|
// readPasswordStdin reads a line from stdin.
|
||||||
// It exits with a fatal error on read error or empty result.
|
// It exits with a fatal error on read error or empty result.
|
||||||
func readPasswordStdin(prompt string) []byte {
|
func readPasswordStdin(prompt string) ([]byte, error) {
|
||||||
tlog.Info.Printf("Reading %s from stdin", prompt)
|
tlog.Info.Printf("Reading %s from stdin", prompt)
|
||||||
p := readLineUnbuffered(os.Stdin)
|
p, err := readLineUnbuffered(os.Stdin)
|
||||||
if len(p) == 0 {
|
if err != nil {
|
||||||
tlog.Fatal.Printf("Got empty %s from stdin", prompt)
|
return nil, err
|
||||||
os.Exit(exitcodes.ReadPassword)
|
|
||||||
}
|
}
|
||||||
return p
|
if len(p) == 0 {
|
||||||
|
return nil, fmt.Errorf("Got empty %s from stdin", prompt)
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// readPasswordExtpass executes the "extpass" program and returns the first line
|
// readPasswordExtpass executes the "extpass" program and returns the first line
|
||||||
// of the output.
|
// of the output.
|
||||||
// Exits on read error or empty result.
|
// Exits on read error or empty result.
|
||||||
func readPasswordExtpass(extpass []string) []byte {
|
func readPasswordExtpass(extpass []string) ([]byte, error) {
|
||||||
var parts []string
|
var parts []string
|
||||||
if len(extpass) == 1 {
|
if len(extpass) == 1 {
|
||||||
parts = strings.Split(extpass[0], " ")
|
parts = strings.Split(extpass[0], " ")
|
||||||
|
@ -109,50 +113,47 @@ func readPasswordExtpass(extpass []string) []byte {
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
pipe, err := cmd.StdoutPipe()
|
pipe, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.Fatal.Printf("extpass pipe setup failed: %v", err)
|
return nil, fmt.Errorf("extpass pipe setup failed: %v", err)
|
||||||
os.Exit(exitcodes.ReadPassword)
|
|
||||||
}
|
}
|
||||||
err = cmd.Start()
|
err = cmd.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.Fatal.Printf("extpass cmd start failed: %v", err)
|
return nil, fmt.Errorf("extpass cmd start failed: %v", err)
|
||||||
os.Exit(exitcodes.ReadPassword)
|
}
|
||||||
|
p, err := readLineUnbuffered(pipe)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
p := readLineUnbuffered(pipe)
|
|
||||||
pipe.Close()
|
pipe.Close()
|
||||||
err = cmd.Wait()
|
err = cmd.Wait()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.Fatal.Printf("extpass program returned an error: %v", err)
|
return nil, fmt.Errorf("extpass program returned an error: %v", err)
|
||||||
os.Exit(exitcodes.ReadPassword)
|
|
||||||
}
|
}
|
||||||
if len(p) == 0 {
|
if len(p) == 0 {
|
||||||
tlog.Fatal.Println("extpass: password is empty")
|
return nil, fmt.Errorf("extpass: password is empty")
|
||||||
os.Exit(exitcodes.ReadPassword)
|
|
||||||
}
|
}
|
||||||
return p
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// readLineUnbuffered reads single bytes from "r" util it gets "\n" or EOF.
|
// readLineUnbuffered reads single bytes from "r" util it gets "\n" or EOF.
|
||||||
// The returned string does NOT contain the trailing "\n".
|
// The returned string does NOT contain the trailing "\n".
|
||||||
func readLineUnbuffered(r io.Reader) (l []byte) {
|
func readLineUnbuffered(r io.Reader) (l []byte, err error) {
|
||||||
b := make([]byte, 1)
|
b := make([]byte, 1)
|
||||||
for {
|
for {
|
||||||
if len(l) > maxPasswordLen {
|
if len(l) > maxPasswordLen {
|
||||||
tlog.Fatal.Printf("fatal: maximum password length of %d bytes exceeded", maxPasswordLen)
|
return nil, fmt.Errorf("fatal: maximum password length of %d bytes exceeded", maxPasswordLen)
|
||||||
os.Exit(exitcodes.ReadPassword)
|
|
||||||
}
|
}
|
||||||
n, err := r.Read(b)
|
n, err := r.Read(b)
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
return l
|
return l, nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.Fatal.Printf("readLineUnbuffered: %v", err)
|
return nil, fmt.Errorf("readLineUnbuffered: %v", err)
|
||||||
os.Exit(exitcodes.ReadPassword)
|
|
||||||
}
|
}
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if b[0] == '\n' {
|
if b[0] == '\n' {
|
||||||
return l
|
return l, nil
|
||||||
}
|
}
|
||||||
l = append(l, b...)
|
l = append(l, b...)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,12 +8,20 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Provide password via stdin, terminated by "\n".
|
// Provide password via stdin, terminated by "\n".
|
||||||
|
//
|
||||||
|
// 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
|
||||||
func TestStdin(t *testing.T) {
|
func TestStdin(t *testing.T) {
|
||||||
p1 := "g55434t55wef"
|
p1 := "g55434t55wef"
|
||||||
if os.Getenv("TEST_SLAVE") == "1" {
|
if os.Getenv("TEST_SLAVE") == "1" {
|
||||||
p2 := string(readPasswordStdin("foo"))
|
p2, err := readPasswordStdin("foo")
|
||||||
if p1 != p2 {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "%q != %q", p1, p2)
|
fmt.Fprint(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if p1 != string(p2) {
|
||||||
|
fmt.Fprintf(os.Stderr, "%q != %q", p1, string(p2))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
@ -41,12 +49,20 @@ func TestStdin(t *testing.T) {
|
||||||
|
|
||||||
// Provide password via stdin, terminated by EOF (pipe close). This should not
|
// Provide password via stdin, terminated by EOF (pipe close). This should not
|
||||||
// hang.
|
// hang.
|
||||||
|
//
|
||||||
|
// 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
|
||||||
func TestStdinEof(t *testing.T) {
|
func TestStdinEof(t *testing.T) {
|
||||||
p1 := "asd45as5f4a36"
|
p1 := "asd45as5f4a36"
|
||||||
if os.Getenv("TEST_SLAVE") == "1" {
|
if os.Getenv("TEST_SLAVE") == "1" {
|
||||||
p2 := string(readPasswordStdin("foo"))
|
p2, err := readPasswordStdin("foo")
|
||||||
if p1 != p2 {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "%q != %q", p1, p2)
|
fmt.Fprint(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if p1 != string(p2) {
|
||||||
|
fmt.Fprintf(os.Stderr, "%q != %q", p1, string(p2))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
@ -76,7 +92,10 @@ func TestStdinEof(t *testing.T) {
|
||||||
// Provide empty password via stdin
|
// Provide empty password via stdin
|
||||||
func TestStdinEmpty(t *testing.T) {
|
func TestStdinEmpty(t *testing.T) {
|
||||||
if os.Getenv("TEST_SLAVE") == "1" {
|
if os.Getenv("TEST_SLAVE") == "1" {
|
||||||
readPasswordStdin("foo")
|
_, err := readPasswordStdin("foo")
|
||||||
|
if err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cmd := exec.Command(os.Args[0], "-test.run=TestStdinEmpty$")
|
cmd := exec.Command(os.Args[0], "-test.run=TestStdinEmpty$")
|
||||||
cmd.Env = append(os.Environ(), "TEST_SLAVE=1")
|
cmd.Env = append(os.Environ(), "TEST_SLAVE=1")
|
||||||
|
|
|
@ -13,6 +13,8 @@ const (
|
||||||
// On MacOS ExFAT, all empty files share inode number 1:
|
// On MacOS ExFAT, all empty files share inode number 1:
|
||||||
// https://github.com/rfjakob/gocryptfs/issues/585
|
// https://github.com/rfjakob/gocryptfs/issues/585
|
||||||
QuirkDuplicateIno1
|
QuirkDuplicateIno1
|
||||||
|
// QuirkNoUserXattr means that user.* xattrs are not supported
|
||||||
|
QuirkNoUserXattr
|
||||||
)
|
)
|
||||||
|
|
||||||
func logQuirk(s string) {
|
func logQuirk(s string) {
|
||||||
|
|
|
@ -27,5 +27,9 @@ func DetectQuirks(cipherdir string) (q uint64) {
|
||||||
q |= QuirkBrokenFalloc
|
q |= QuirkBrokenFalloc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if uint32(st.Type) == unix.TMPFS_MAGIC {
|
||||||
|
logQuirk("tmpfs detected, no extended attributes except acls will work.")
|
||||||
|
}
|
||||||
|
|
||||||
return q
|
return q
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,11 @@ const (
|
||||||
// O_PATH is only defined on Linux
|
// O_PATH is only defined on Linux
|
||||||
O_PATH = 0
|
O_PATH = 0
|
||||||
|
|
||||||
// RENAME_NOREPLACE is only defined on Linux
|
// Only exists on Linux. Define here to fix build failure, even though
|
||||||
RENAME_NOREPLACE = 0
|
// we will never see the flags.
|
||||||
|
RENAME_NOREPLACE = 1
|
||||||
|
RENAME_EXCHANGE = 2
|
||||||
|
RENAME_WHITEOUT = 4
|
||||||
|
|
||||||
// KAUTH_UID_NONE and KAUTH_GID_NONE are special values to
|
// KAUTH_UID_NONE and KAUTH_GID_NONE are special values to
|
||||||
// revert permissions to the process credentials.
|
// revert permissions to the process credentials.
|
||||||
|
|
|
@ -28,8 +28,10 @@ const (
|
||||||
// O_PATH is only defined on Linux
|
// O_PATH is only defined on Linux
|
||||||
O_PATH = unix.O_PATH
|
O_PATH = unix.O_PATH
|
||||||
|
|
||||||
// RENAME_NOREPLACE is only defined on Linux
|
// Only defined on Linux
|
||||||
RENAME_NOREPLACE = unix.RENAME_NOREPLACE
|
RENAME_NOREPLACE = unix.RENAME_NOREPLACE
|
||||||
|
RENAME_WHITEOUT = unix.RENAME_WHITEOUT
|
||||||
|
RENAME_EXCHANGE = unix.RENAME_EXCHANGE
|
||||||
)
|
)
|
||||||
|
|
||||||
var preallocWarn sync.Once
|
var preallocWarn sync.Once
|
||||||
|
|
14
main.go
14
main.go
|
@ -55,11 +55,15 @@ func loadConfig(args *argContainer) (masterkey []byte, cf *configfile.ConfFile,
|
||||||
if cf.IsFeatureFlagSet(configfile.FlagFIDO2) {
|
if cf.IsFeatureFlagSet(configfile.FlagFIDO2) {
|
||||||
if args.fido2 == "" {
|
if args.fido2 == "" {
|
||||||
tlog.Fatal.Printf("Masterkey encrypted using FIDO2 token; need to use the --fido2 option.")
|
tlog.Fatal.Printf("Masterkey encrypted using FIDO2 token; need to use the --fido2 option.")
|
||||||
os.Exit(exitcodes.Usage)
|
return nil, nil, exitcodes.NewErr("", exitcodes.Usage)
|
||||||
}
|
}
|
||||||
pw = fido2.Secret(args.fido2, cf.FIDO2.CredentialID, cf.FIDO2.HMACSalt)
|
pw = fido2.Secret(args.fido2, cf.FIDO2.CredentialID, cf.FIDO2.HMACSalt)
|
||||||
} else {
|
} else {
|
||||||
pw = readpassword.Once([]string(args.extpass), []string(args.passfile), "")
|
pw, err = readpassword.Once([]string(args.extpass), []string(args.passfile), "")
|
||||||
|
if err != nil {
|
||||||
|
tlog.Fatal.Println(err)
|
||||||
|
return nil, nil, exitcodes.NewErr("", exitcodes.ReadPassword)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tlog.Info.Println("Decrypting master key")
|
tlog.Info.Println("Decrypting master key")
|
||||||
masterkey, err = cf.DecryptMasterKey(pw)
|
masterkey, err = cf.DecryptMasterKey(pw)
|
||||||
|
@ -93,7 +97,11 @@ func changePassword(args *argContainer) {
|
||||||
os.Exit(exitcodes.Usage)
|
os.Exit(exitcodes.Usage)
|
||||||
}
|
}
|
||||||
tlog.Info.Println("Please enter your new password.")
|
tlog.Info.Println("Please enter your new password.")
|
||||||
newPw := readpassword.Twice([]string(args.extpass), []string(args.passfile))
|
newPw, err := readpassword.Twice([]string(args.extpass), []string(args.passfile))
|
||||||
|
if err != nil {
|
||||||
|
tlog.Fatal.Println(err)
|
||||||
|
os.Exit(exitcodes.ReadPassword)
|
||||||
|
}
|
||||||
logN := confFile.ScryptObject.LogN()
|
logN := confFile.ScryptObject.LogN()
|
||||||
if args._explicitScryptn {
|
if args._explicitScryptn {
|
||||||
logN = args.scryptn
|
logN = args.scryptn
|
||||||
|
|
|
@ -39,8 +39,12 @@ func unhexMasterKey(masterkey string, fromStdin bool) []byte {
|
||||||
func handleArgsMasterkey(args *argContainer) (masterkey []byte) {
|
func handleArgsMasterkey(args *argContainer) (masterkey []byte) {
|
||||||
// "-masterkey=stdin"
|
// "-masterkey=stdin"
|
||||||
if args.masterkey == "stdin" {
|
if args.masterkey == "stdin" {
|
||||||
in := string(readpassword.Once(nil, nil, "Masterkey"))
|
in, err := readpassword.Once(nil, nil, "Masterkey")
|
||||||
return unhexMasterKey(in, true)
|
if err != nil {
|
||||||
|
tlog.Fatal.Println(err)
|
||||||
|
os.Exit(exitcodes.ReadPassword)
|
||||||
|
}
|
||||||
|
return unhexMasterKey(string(in), true)
|
||||||
}
|
}
|
||||||
// "-masterkey=941a6029-3adc6a1c-..."
|
// "-masterkey=941a6029-3adc6a1c-..."
|
||||||
if args.masterkey != "" {
|
if args.masterkey != "" {
|
||||||
|
|
33
test.bash
33
test.bash
|
@ -1,14 +1,23 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
if [[ -z $TMPDIR ]]; then
|
set -eu
|
||||||
|
|
||||||
|
VERBOSE=0
|
||||||
|
for i in "$@" ; do
|
||||||
|
if [[ $i == "-v" ]] ; then
|
||||||
|
VERBOSE=1
|
||||||
|
set -x
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z ${TMPDIR:-} ]]; then
|
||||||
TMPDIR=/var/tmp
|
TMPDIR=/var/tmp
|
||||||
export TMPDIR
|
export TMPDIR
|
||||||
else
|
else
|
||||||
echo "Using TMPDIR=$TMPDIR"
|
echo "Using TMPDIR=$TMPDIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
export GO111MODULE=on
|
export GO111MODULE=on
|
||||||
MYNAME=$(basename "$0")
|
MYNAME=$(basename "$0")
|
||||||
|
@ -53,7 +62,7 @@ if ! go tool | grep vet > /dev/null ; then
|
||||||
elif [[ -d vendor ]] ; then
|
elif [[ -d vendor ]] ; then
|
||||||
echo "vendor directory exists, skipping 'go tool vet'"
|
echo "vendor directory exists, skipping 'go tool vet'"
|
||||||
else
|
else
|
||||||
go vet "$@" ./...
|
go vet ./...
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if command -v shellcheck > /dev/null ; then
|
if command -v shellcheck > /dev/null ; then
|
||||||
|
@ -63,10 +72,18 @@ else
|
||||||
echo "shellcheck not installed - skipping"
|
echo "shellcheck not installed - skipping"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# We don't want all the subprocesses
|
EXTRA_ARGS=""
|
||||||
# holding the lock file open
|
if [[ $VERBOSE -eq 1 ]]; then
|
||||||
# vvvvv
|
# Disabling parallelism disables per-package output buffering, hence enabling
|
||||||
go test -count 1 ./... "$@" 200>&-
|
# live streaming of result output. And seeing where things hang.
|
||||||
|
EXTRA_ARGS="-p 1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# We don't want all the subprocesses
|
||||||
|
# holding the lock file open
|
||||||
|
# vvvvv
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
go test -count 1 $EXTRA_ARGS ./... "$@" 200>&-
|
||||||
# ^^^^^^^^
|
# ^^^^^^^^
|
||||||
# Disable result caching
|
# Disable result caching
|
||||||
|
|
||||||
|
|
|
@ -398,15 +398,37 @@ func TestShadows(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMountPasswordIncorrect makes sure the correct exit code is used when the password
|
// TestMountPasswordIncorrect makes sure the correct exit code is used when the password
|
||||||
// was incorrect while mounting
|
// was incorrect while mounting.
|
||||||
|
// Also checks that we don't leave a socket file behind.
|
||||||
func TestMountPasswordIncorrect(t *testing.T) {
|
func TestMountPasswordIncorrect(t *testing.T) {
|
||||||
cDir := test_helpers.InitFS(t) // Create filesystem with password "test"
|
cDir := test_helpers.InitFS(t) // Create filesystem with password "test"
|
||||||
|
ctlSock := cDir + ".sock"
|
||||||
pDir := cDir + ".mnt"
|
pDir := cDir + ".mnt"
|
||||||
err := test_helpers.Mount(cDir, pDir, false, "-extpass", "echo WRONG", "-wpanic=false")
|
err := test_helpers.Mount(cDir, pDir, false, "-extpass", "echo WRONG", "-wpanic=false", "-ctlsock", ctlSock)
|
||||||
exitCode := test_helpers.ExtractCmdExitCode(err)
|
exitCode := test_helpers.ExtractCmdExitCode(err)
|
||||||
if exitCode != exitcodes.PasswordIncorrect {
|
if exitCode != exitcodes.PasswordIncorrect {
|
||||||
t.Errorf("want=%d, got=%d", exitcodes.PasswordIncorrect, exitCode)
|
t.Errorf("want=%d, got=%d", exitcodes.PasswordIncorrect, exitCode)
|
||||||
}
|
}
|
||||||
|
if _, err := os.Stat(ctlSock); err == nil {
|
||||||
|
t.Errorf("socket file %q left behind", ctlSock)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMountPasswordEmpty makes sure the correct exit code is used when the password
|
||||||
|
// was empty while mounting.
|
||||||
|
// Also checks that we don't leave a socket file behind (https://github.com/rfjakob/gocryptfs/issues/634).
|
||||||
|
func TestMountPasswordEmpty(t *testing.T) {
|
||||||
|
cDir := test_helpers.InitFS(t) // Create filesystem with password "test"
|
||||||
|
ctlSock := cDir + ".sock"
|
||||||
|
pDir := cDir + ".mnt"
|
||||||
|
err := test_helpers.Mount(cDir, pDir, false, "-extpass", "true", "-wpanic=false", "-ctlsock", ctlSock)
|
||||||
|
exitCode := test_helpers.ExtractCmdExitCode(err)
|
||||||
|
if exitCode != exitcodes.ReadPassword {
|
||||||
|
t.Errorf("want=%d, got=%d", exitcodes.ReadPassword, exitCode)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(ctlSock); err == nil {
|
||||||
|
t.Errorf("socket file %q left behind", ctlSock)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestPasswdPasswordIncorrect makes sure the correct exit code is used when the password
|
// TestPasswdPasswordIncorrect makes sure the correct exit code is used when the password
|
||||||
|
|
|
@ -14,6 +14,8 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
"github.com/rfjakob/gocryptfs/v2/tests/test_helpers"
|
"github.com/rfjakob/gocryptfs/v2/tests/test_helpers"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -267,15 +269,15 @@ func TestCpWarnings(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSeekData tests that fs.FileLseeker is implemented
|
// TestSeekData tests that SEEK_DATA works
|
||||||
func TestSeekData(t *testing.T) {
|
func TestSeekData(t *testing.T) {
|
||||||
fn := filepath.Join(test_helpers.DefaultPlainDir, t.Name())
|
fn := filepath.Join(test_helpers.DefaultPlainDir, t.Name())
|
||||||
f, err := os.Create(fn)
|
f, err := os.Create(fn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
var oneTiB int64 = 1024 * 1024 * 1024 * 1024
|
var dataOffset int64 = 1024 * 1024 * 1024 // 1 GiB
|
||||||
if _, err = f.Seek(oneTiB, 0); err != nil {
|
if _, err = f.Seek(dataOffset, 0); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if _, err = f.Write([]byte("foo")); err != nil {
|
if _, err = f.Write([]byte("foo")); err != nil {
|
||||||
|
@ -283,18 +285,16 @@ func TestSeekData(t *testing.T) {
|
||||||
}
|
}
|
||||||
f.Close()
|
f.Close()
|
||||||
|
|
||||||
const SEEK_DATA = 3
|
|
||||||
|
|
||||||
f, err = os.Open(fn)
|
f, err = os.Open(fn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
off, err := f.Seek(1024*1024, SEEK_DATA)
|
off, err := f.Seek(1024*1024, unix.SEEK_DATA)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if off < oneTiB-1024*1024 {
|
if off < dataOffset-1024*1024 {
|
||||||
t.Errorf("off=%d, expected=%d\n", off, oneTiB)
|
t.Errorf("off=%d, expected=%d\n", off, dataOffset)
|
||||||
}
|
}
|
||||||
f.Close()
|
f.Close()
|
||||||
}
|
}
|
||||||
|
@ -427,10 +427,11 @@ func TestFsync(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// force_owner was broken by the v2.0 rewrite:
|
// force_owner was broken by the v2.0 rewrite:
|
||||||
// The owner was only forced for GETATTR, but not for CREATE or LOOKUP.
|
// The owner was only forced for GETATTR, but not for CREATE, LOOKUP, MKNOD.
|
||||||
//
|
//
|
||||||
// https://github.com/rfjakob/gocryptfs/issues/609
|
// https://github.com/rfjakob/gocryptfs/issues/609
|
||||||
// https://github.com/rfjakob/gocryptfs/pull/610
|
// https://github.com/rfjakob/gocryptfs/pull/610
|
||||||
|
// https://github.com/rfjakob/gocryptfs/issues/629
|
||||||
func TestForceOwner(t *testing.T) {
|
func TestForceOwner(t *testing.T) {
|
||||||
cDir := test_helpers.InitFS(t)
|
cDir := test_helpers.InitFS(t)
|
||||||
os.Chmod(cDir, 0777) // Mount needs to be accessible for us
|
os.Chmod(cDir, 0777) // Mount needs to be accessible for us
|
||||||
|
@ -479,6 +480,18 @@ func TestForceOwner(t *testing.T) {
|
||||||
t.Errorf("GETATTR returned uid or gid != 1234: %#v", st)
|
t.Errorf("GETATTR returned uid or gid != 1234: %#v", st)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test MKNOD
|
||||||
|
sock := pDir + "/sock"
|
||||||
|
if err := syscall.Mknod(sock, syscall.S_IFSOCK|0600, 0); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := syscall.Stat(sock, &st); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if st.Uid != 1234 || st.Gid != 1234 {
|
||||||
|
t.Errorf("MKNOD returned uid or gid != 1234: %#v", st)
|
||||||
|
}
|
||||||
|
|
||||||
// Remount to clear cache
|
// Remount to clear cache
|
||||||
test_helpers.UnmountPanic(pDir)
|
test_helpers.UnmountPanic(pDir)
|
||||||
test_helpers.MountOrFatal(t, cDir, pDir, "-force_owner=1234:1234", "-extpass=echo test")
|
test_helpers.MountOrFatal(t, cDir, pDir, "-force_owner=1234:1234", "-extpass=echo test")
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
// +build linux
|
||||||
|
|
||||||
|
package defaults
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
"github.com/rfjakob/gocryptfs/v2/internal/syscallcompat"
|
||||||
|
"github.com/rfjakob/gocryptfs/v2/tests/test_helpers"
|
||||||
|
)
|
||||||
|
|
||||||
|
// https://github.com/rfjakob/gocryptfs/issues/641
|
||||||
|
//
|
||||||
|
// I was trying to run the Docker daemon with the recommended overlay2 storage driver, and encrypt its `/var/lib/docker` directory using gocryptfs. overlay2 was giving me the following errors:
|
||||||
|
// ```
|
||||||
|
// Jan 21 19:09:43 friedhelm.rankenste.in kernel: overlayfs: upper fs does not support tmpfile.
|
||||||
|
// Jan 21 19:09:43 friedhelm.rankenste.in kernel: overlayfs: upper fs does not support RENAME_WHITEOUT.
|
||||||
|
// Jan 21 19:09:43 friedhelm.rankenste.in kernel: overlayfs: upper fs missing required features.
|
||||||
|
// ```
|
||||||
|
|
||||||
|
func TestRenameWhiteout(t *testing.T) {
|
||||||
|
short := t.Name() + ".short"
|
||||||
|
long := t.Name() + strings.Repeat(".long", 200/len(".long"))
|
||||||
|
|
||||||
|
names := [][]string{
|
||||||
|
// short to short
|
||||||
|
{short + "s2s", short + "s2s2"},
|
||||||
|
// short to long
|
||||||
|
{short + "s2l", long + "s2l2"},
|
||||||
|
// long to short
|
||||||
|
{long + "l2s", short + "l2s2"},
|
||||||
|
// long to long
|
||||||
|
{long + "l2l", short + "l2l2"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, flags := range []uint{syscallcompat.RENAME_WHITEOUT, syscallcompat.RENAME_WHITEOUT | syscallcompat.RENAME_NOREPLACE} {
|
||||||
|
for _, n := range names {
|
||||||
|
pSrc := test_helpers.DefaultPlainDir + "/" + n[0]
|
||||||
|
pDst := test_helpers.DefaultPlainDir + "/" + n[1]
|
||||||
|
if err := ioutil.WriteFile(pSrc, nil, 0200); err != nil {
|
||||||
|
t.Fatalf("creating empty file failed: %v", err)
|
||||||
|
}
|
||||||
|
err := unix.Renameat2(-1, pSrc, -1, pDst, flags)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
// readdir should not choke on leftover or missing .name files
|
||||||
|
dir, err := os.Open(test_helpers.DefaultPlainDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer dir.Close()
|
||||||
|
_, err = dir.Readdir(0)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
// pSrc should now be a character device 0 file
|
||||||
|
var st unix.Stat_t
|
||||||
|
err = unix.Stat(pSrc, &st)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if !(st.Mode&unix.S_IFMT == unix.S_IFCHR) {
|
||||||
|
t.Error("not a device file")
|
||||||
|
}
|
||||||
|
if st.Rdev != 0 {
|
||||||
|
t.Errorf("want device 0, have %d", st.Rdev)
|
||||||
|
}
|
||||||
|
unix.Unlink(pSrc)
|
||||||
|
unix.Unlink(pDst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenameExchange(t *testing.T) {
|
||||||
|
short := t.Name() + ".short"
|
||||||
|
long := t.Name() + strings.Repeat(".long", 200/len(".long"))
|
||||||
|
|
||||||
|
names := [][]string{
|
||||||
|
// short to short
|
||||||
|
{short + "s2s", short + "s2s2"},
|
||||||
|
// short to long
|
||||||
|
{short + "s2l", long + "s2l2"},
|
||||||
|
// long to short
|
||||||
|
{long + "l2s", short + "l2s2"},
|
||||||
|
// long to long
|
||||||
|
{long + "l2l", short + "l2l2"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, n := range names {
|
||||||
|
pSrc := test_helpers.DefaultPlainDir + "/" + n[0]
|
||||||
|
pDst := test_helpers.DefaultPlainDir + "/" + n[1]
|
||||||
|
if err := ioutil.WriteFile(pSrc, nil, 0200); err != nil {
|
||||||
|
t.Fatalf("creating empty file failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := ioutil.WriteFile(pDst, nil, 0200); err != nil {
|
||||||
|
t.Fatalf("creating empty file failed: %v", err)
|
||||||
|
}
|
||||||
|
err := unix.Renameat2(-1, pSrc, -1, pDst, unix.RENAME_EXCHANGE)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
// readdir should not choke on leftover or missing .name files
|
||||||
|
dir, err := os.Open(test_helpers.DefaultPlainDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer dir.Close()
|
||||||
|
_, err = dir.Readdir(0)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looks like the FUSE protocol does support O_TMPFILE yet
|
||||||
|
func TestOTmpfile(t *testing.T) {
|
||||||
|
p := test_helpers.DefaultPlainDir + "/" + t.Name()
|
||||||
|
fd, err := unix.Openat(-1, p, unix.O_TMPFILE, 0600)
|
||||||
|
if err != nil {
|
||||||
|
t.Skip(err)
|
||||||
|
}
|
||||||
|
unix.Close(fd)
|
||||||
|
}
|
|
@ -259,7 +259,7 @@ func TestStatfs(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSeekData tests that fs.FileLseeker is implemented
|
// TestSeekData tests that SEEK_DATA works
|
||||||
func TestSeekData(t *testing.T) {
|
func TestSeekData(t *testing.T) {
|
||||||
if !plaintextnames {
|
if !plaintextnames {
|
||||||
t.Skip()
|
t.Skip()
|
||||||
|
@ -270,8 +270,8 @@ func TestSeekData(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
var oneTiB int64 = 1024 * 1024 * 1024 * 1024
|
var dataOffset int64 = 1 * 1024 * 1024 * 1024 // 1 GiB
|
||||||
if _, err = f.Seek(oneTiB, 0); err != nil {
|
if _, err = f.Seek(dataOffset, 0); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if _, err = f.Write([]byte("foo")); err != nil {
|
if _, err = f.Write([]byte("foo")); err != nil {
|
||||||
|
@ -279,19 +279,17 @@ func TestSeekData(t *testing.T) {
|
||||||
}
|
}
|
||||||
f.Close()
|
f.Close()
|
||||||
|
|
||||||
const SEEK_DATA = 3
|
|
||||||
|
|
||||||
fn2 := filepath.Join(dirB, t.Name())
|
fn2 := filepath.Join(dirB, t.Name())
|
||||||
f, err = os.Open(fn2)
|
f, err = os.Open(fn2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
off, err := f.Seek(1024*1024, SEEK_DATA)
|
off, err := f.Seek(1024*1024, unix.SEEK_DATA)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if off < oneTiB-1024*1024 {
|
if off < dataOffset-1024*1024 {
|
||||||
t.Errorf("off=%d, expected=%d\n", off, oneTiB)
|
t.Errorf("off=%d, expected=%d\n", off, dataOffset)
|
||||||
}
|
}
|
||||||
f.Close()
|
f.Close()
|
||||||
}
|
}
|
||||||
|
|
|
@ -179,7 +179,13 @@ func TestDiskFull(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer syscall.Unlink(ext4img)
|
defer syscall.Unlink(ext4img)
|
||||||
defer syscall.Unmount(ext4mnt, 0)
|
defer func() {
|
||||||
|
const MNT_DETACH = 2
|
||||||
|
err := syscall.Unmount(ext4mnt, MNT_DETACH)
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// gocryptfs -init
|
// gocryptfs -init
|
||||||
cipherdir := ext4mnt + "/a"
|
cipherdir := ext4mnt + "/a"
|
||||||
|
@ -362,3 +368,34 @@ func TestBtrfsQuirks(t *testing.T) {
|
||||||
t.Errorf("wrong quirk: %v", quirk)
|
t.Errorf("wrong quirk: %v", quirk)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOverlay(t *testing.T) {
|
||||||
|
if os.Getuid() != 0 {
|
||||||
|
t.Skip("must run as root")
|
||||||
|
}
|
||||||
|
cDir := test_helpers.InitFS(t)
|
||||||
|
if syscallcompat.DetectQuirks(cDir)|syscallcompat.QuirkNoUserXattr != 0 {
|
||||||
|
t.Logf("No user xattrs! overlay mount will likely fail.")
|
||||||
|
}
|
||||||
|
os.Chmod(cDir, 0755)
|
||||||
|
pDir := cDir + ".mnt"
|
||||||
|
test_helpers.MountOrFatal(t, cDir, pDir, "-allow_other", "-extpass=echo test")
|
||||||
|
defer test_helpers.UnmountPanic(pDir)
|
||||||
|
|
||||||
|
for _, d := range []string{"lower", "upper", "work", "merged"} {
|
||||||
|
err := os.Mkdir(pDir+"/"+d, 0700)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ovlMnt := pDir + "/merged"
|
||||||
|
cmd := exec.Command("mount", "-t", "overlay", "overlay",
|
||||||
|
"-o", "lowerdir="+pDir+"/lower,upperdir="+pDir+"/upper,workdir="+pDir+"/work",
|
||||||
|
ovlMnt)
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Log(string(out))
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer syscall.Unmount(ovlMnt, 0)
|
||||||
|
}
|
||||||
|
|
|
@ -37,7 +37,10 @@ type mountInfo struct {
|
||||||
func Mount(c string, p string, showOutput bool, extraArgs ...string) error {
|
func Mount(c string, p string, showOutput bool, extraArgs ...string) error {
|
||||||
args := []string{"-q", "-wpanic", "-nosyslog", "-fg", fmt.Sprintf("-notifypid=%d", os.Getpid())}
|
args := []string{"-q", "-wpanic", "-nosyslog", "-fg", fmt.Sprintf("-notifypid=%d", os.Getpid())}
|
||||||
args = append(args, extraArgs...)
|
args = append(args, extraArgs...)
|
||||||
//args = append(args, "-fusedebug")
|
if _, isset := os.LookupEnv("FUSEDEBUG"); isset {
|
||||||
|
fmt.Println("FUSEDEBUG is set, enabling -fusedebug")
|
||||||
|
args = append(args, "-fusedebug")
|
||||||
|
}
|
||||||
//args = append(args, "-d")
|
//args = append(args, "-d")
|
||||||
args = append(args, c, p)
|
args = append(args, c, p)
|
||||||
|
|
||||||
|
|
|
@ -369,3 +369,17 @@ func TestAcl(t *testing.T) {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestSlashInName checks that slashes in xattr names are allowed
|
||||||
|
// https://github.com/rfjakob/gocryptfs/issues/627
|
||||||
|
func TestSlashInName(t *testing.T) {
|
||||||
|
fn := test_helpers.DefaultPlainDir + "/" + t.Name()
|
||||||
|
err := ioutil.WriteFile(fn, []byte("12345"), 0700)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("creating empty file failed: %v", err)
|
||||||
|
}
|
||||||
|
err = setGetRmList3(fn, "user.foo@https://bar", []byte("val"))
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue