Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion cli_args.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ type argContainer struct {
longnames, allow_other, reverse, aessiv, nonempty, raw64,
noprealloc, speed, hkdf, serialize_reads, hh, info,
sharedstorage, fsck, one_file_system, deterministic_names,
xchacha bool
xchacha, takeout, list bool
// Mount options with opposites
dev, nodev, suid, nosuid, exec, noexec, rw, ro, kernel_cache, acl bool
masterkey, mountpoint, cipherdir, cpuprofile,
Expand Down Expand Up @@ -188,6 +188,8 @@ func parseCliOpts(osArgs []string) (args argContainer) {
flagSet.BoolVar(&args.one_file_system, "one-file-system", false, "Don't cross filesystem boundaries")
flagSet.BoolVar(&args.deterministic_names, "deterministic-names", false, "Disable diriv file name randomisation")
flagSet.BoolVar(&args.xchacha, "xchacha", false, "Use XChaCha20-Poly1305 file content encryption")
flagSet.BoolVar(&args.takeout, "takeout", false, "Decrypt and move files out of the encrypted directory")
flagSet.BoolVar(&args.list, "list", false, "List files in the encrypted directory")

// Mount options with opposites
flagSet.BoolVar(&args.dev, "dev", false, "Allow device files")
Expand Down Expand Up @@ -336,6 +338,12 @@ func countOpFlags(args *argContainer) int {
if args.fsck {
count++
}
if args.takeout {
count++
}
if args.list {
count++
}
return count
}

Expand Down
2 changes: 2 additions & 0 deletions internal/exitcodes/exitcodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ const (
DevNull = 30
// FIDO2Error - an error was encountered while interacting with a FIDO2 token
FIDO2Error = 31
// Walk - an error was encountered while walking a directory
Walk = 32
)

// Err wraps an error with an associated numeric exit code
Expand Down
15 changes: 15 additions & 0 deletions internal/nametransform/diriv.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package nametransform

import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"syscall"

"github.com/rfjakob/gocryptfs/v2/internal/cryptocore"
Expand Down Expand Up @@ -96,3 +98,16 @@ func WriteDirIVAt(dirfd int) error {
}
return nil
}

// ReadDirIV reads the DirIV from "dir"/gocryptfs.diriv.
func ReadDirIV(dir string) ([]byte, error) {
ivPath := filepath.Join(dir, DirIVFilename)
iv, err := os.ReadFile(ivPath)
if err != nil {
return nil, err
}
if len(iv) != DirIVLen {
return nil, errors.New("DirIV has wrong length")
}
return iv, nil
}
23 changes: 23 additions & 0 deletions internal/nametransform/names.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/base64"
"errors"
"math"
"os"
"path/filepath"
"strings"
"syscall"
Expand Down Expand Up @@ -183,3 +184,25 @@ func Dir(path string) string {
func (n *NameTransform) GetLongNameMax() int {
return n.longNameMax
}

// IsLongName checks if `cipherName` is a longname file.
func IsLongName(cipherName string) bool {
return strings.HasPrefix(cipherName, "gocryptfs.longname.")
}

// ReadLongName reads the content of a longname file.
func ReadLongName(longNamePath string) (string, error) {
content, err := os.ReadFile(longNamePath)
if err != nil {
return "", err
}
return string(content), nil
}

// IsValidBase64 checks if a string is a valid base64 encoding.
func IsValidBase64(s string) bool {
_, err := base64.URLEncoding.DecodeString(s)
return err == nil
}


125 changes: 125 additions & 0 deletions list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package main

import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/rfjakob/gocryptfs/v2/internal/configfile"
"github.com/rfjakob/gocryptfs/v2/internal/cryptocore"
"github.com/rfjakob/gocryptfs/v2/internal/exitcodes"
"github.com/rfjakob/gocryptfs/v2/internal/nametransform"
"github.com/rfjakob/gocryptfs/v2/internal/tlog"
)

func list(args *argContainer) {
if flagSet.NArg() != 1 {
tlog.Fatal.Printf("Usage: %s -list CIPHERDIR", tlog.ProgramName)
os.Exit(exitcodes.Usage)
}
cipherdir := flagSet.Arg(0)

masterkey, _, err := loadConfig(args)
if err != nil {
exitcodes.Exit(err)
}

var aeadType cryptocore.AEADTypeEnum
if args.aessiv {
aeadType = cryptocore.BackendAESSIV
} else if args.xchacha {
if args.openssl {
aeadType = cryptocore.BackendXChaCha20Poly1305OpenSSL
} else {
aeadType = cryptocore.BackendXChaCha20Poly1305
}
} else {
if args.openssl {
aeadType = cryptocore.BackendOpenSSL
} else {
aeadType = cryptocore.BackendGoGCM
}
}
cCore := cryptocore.New(masterkey, aeadType, 128, args.hkdf)
nameTransform := nametransform.New(cCore.EMECipher, args.longnames, args.longnamemax, args.raw64, args.badname, args.deterministic_names)

err = filepath.Walk(cipherdir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}

// Get the relative path from the cipherdir
relPath, err := filepath.Rel(cipherdir, path)
if err != nil {
return err
}

// Skip the root directory itself
if relPath == "." {
return nil
}

// Skip special files
if info.Name() == nametransform.DirIVFilename || info.Name() == configfile.ConfDefaultName || info.Name() == configfile.ConfReverseName {
return nil
}

// Only list files, not directories, for "git ls-files" behavior
if info.IsDir() {
return nil
}

// Split the relative encrypted path into components
encryptedComponents := strings.Split(relPath, string(filepath.Separator))
decryptedPathParts := make([]string, len(encryptedComponents))

// Iterate through components to decrypt each part
for i, comp := range encryptedComponents {
var iv []byte
var errIV error

// Determine the encrypted parent directory for the current component
var encryptedParentDir string
if i == 0 {
// For the first component, the parent is the cipherdir itself
encryptedParentDir = cipherdir
} else {
// For subsequent components, the parent is the path formed by previous encrypted components
encryptedParentDir = filepath.Join(cipherdir, filepath.Join(encryptedComponents[:i]...))
}

// Read the IV for the parent directory
iv, errIV = nametransform.ReadDirIV(encryptedParentDir)
if errIV != nil {
if os.IsNotExist(errIV) || args.plaintextnames {
// In plaintextnames mode, or if gocryptfs.diriv is missing, use an all-zero IV.
iv = make([]byte, nametransform.DirIVLen)
} else {
tlog.Warn.Printf("Failed to read IV for parent %q of component %q: %v", encryptedParentDir, comp, errIV)
return nil // Skip this path if IV cannot be read
}
}

// Decrypt the current component name
decryptedComp, errDecrypt := nameTransform.DecryptName(comp, iv)
if errDecrypt != nil {
tlog.Warn.Printf("Failed to decrypt component %q (full encrypted path: %q): %v", comp, path, errDecrypt)
return nil // Skip this path if decryption fails
}
decryptedPathParts[i] = decryptedComp
}

// Join the decrypted components to form the full relative decrypted path
fullDecryptedPath := filepath.Join(decryptedPathParts...)
fmt.Println(fullDecryptedPath)

return nil
})

if err != nil {
tlog.Fatal.Printf("Error walking directory: %v", err)
os.Exit(exitcodes.Walk)
}
}

31 changes: 26 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,13 +272,24 @@ func main() {
// Don't call os.Exit to give deferred functions a chance to run
return
}
if nOps > 1 {
tlog.Fatal.Printf("At most one of -info, -init, -passwd, -fsck is allowed")
if nOps > 1 {
tlog.Fatal.Printf("At most one of -info, -init, -passwd, -fsck, -takeout, -list is allowed")
os.Exit(exitcodes.Usage)
}
if flagSet.NArg() != 1 {
tlog.Fatal.Printf("The options -info, -init, -passwd, -fsck take exactly one argument, %d given",
flagSet.NArg())

// Check argument count
expectedNArg := 1
if args.takeout {
expectedNArg = 3
}
if flagSet.NArg() != expectedNArg {
if args.takeout {
tlog.Fatal.Printf("The option -takeout takes exactly three arguments (CIPHERDIR, PATH, DESTDIR), %d given", flagSet.NArg())
} else if args.list {
tlog.Fatal.Printf("The option -list takes exactly one argument (CIPHERDIR), %d given", flagSet.NArg())
} else {
tlog.Fatal.Printf("The options -info, -init, -passwd, -fsck take exactly one argument, %d given", flagSet.NArg())
}
os.Exit(exitcodes.Usage)
}
// "-info"
Expand All @@ -301,4 +312,14 @@ func main() {
code := fsck(&args)
os.Exit(code)
}
// "-takeout"
if args.takeout {
takeOut(&args)
os.Exit(0)
}
// "-list"
if args.list {
list(&args)
os.Exit(0)
}
}
Loading