github.com/StackExchange/blackbox/v2@v2.0.1-0.20220331193400-d84e904973ab/pkg/box/boxutils.go (about)

     1  package box
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"os"
     7  	"os/user"
     8  	"path/filepath"
     9  	"runtime"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"github.com/StackExchange/blackbox/v2/pkg/makesafe"
    14  )
    15  
    16  // FileStatus returns the status of a file.
    17  func FileStatus(name string) (string, error) {
    18  	/*
    19  		DECRYPTED: File is decrypted and ready to edit (unknown if it has been edited).
    20  		ENCRYPTED: GPG file is newer than plaintext. Indicates recented edited then encrypted.
    21  		SHREDDED: Plaintext is missing.
    22  		GPGMISSING: The .gpg file is missing. Oops?
    23  		PLAINERROR: Can't access the plaintext file to determine status.
    24  		GPGERROR: Can't access .gpg file to determine status.
    25  	*/
    26  
    27  	p := name
    28  	e := p + ".gpg"
    29  	ps, perr := os.Stat(p)
    30  	es, eerr := os.Stat(e)
    31  	if perr == nil && eerr == nil {
    32  		if ps.ModTime().Before(es.ModTime()) {
    33  			return "ENCRYPTED", nil
    34  		}
    35  		return "DECRYPTED", nil
    36  	}
    37  
    38  	if os.IsNotExist(perr) && os.IsNotExist(eerr) {
    39  		return "BOTHMISSING", nil
    40  	}
    41  
    42  	if eerr != nil {
    43  		if os.IsNotExist(eerr) {
    44  			return "GPGMISSING", nil
    45  		}
    46  		return "GPGERROR", eerr
    47  	}
    48  
    49  	if perr != nil {
    50  		if os.IsNotExist(perr) {
    51  			return "SHREDDED", nil
    52  		}
    53  	}
    54  	return "PLAINERROR", perr
    55  }
    56  
    57  func anyGpg(names []string) error {
    58  	for _, name := range names {
    59  		if strings.HasSuffix(name, ".gpg") {
    60  			return fmt.Errorf(
    61  				"no not specify .gpg files. Specify %q not %q",
    62  				strings.TrimSuffix(name, ".gpg"), name)
    63  		}
    64  	}
    65  	return nil
    66  }
    67  
    68  // func isChanged(pname string) (bool, error) {
    69  // 	// if .gpg exists but not plainfile: unchanged
    70  // 	// if plaintext exists but not .gpg: changed
    71  // 	// if plainfile < .gpg: unchanged
    72  // 	// if plainfile > .gpg: don't know, need to try diff
    73  
    74  // 	// Gather info about the files:
    75  
    76  // 	pstat, perr := os.Stat(pname)
    77  // 	if perr != nil && (!os.IsNotExist(perr)) {
    78  // 		return false, fmt.Errorf("isChanged(%q) returned error: %w", pname, perr)
    79  // 	}
    80  // 	gname := pname + ".gpg"
    81  // 	gstat, gerr := os.Stat(gname)
    82  // 	if gerr != nil && (!os.IsNotExist(perr)) {
    83  // 		return false, fmt.Errorf("isChanged(%q) returned error: %w", gname, gerr)
    84  // 	}
    85  
    86  // 	pexists := perr == nil
    87  // 	gexists := gerr == nil
    88  
    89  // 	// Use the above rules:
    90  
    91  // 	// if .gpg exists but not plainfile: unchanged
    92  // 	if gexists && !pexists {
    93  // 		return false, nil
    94  // 	}
    95  
    96  // 	// if plaintext exists but not .gpg: changed
    97  // 	if pexists && !gexists {
    98  // 		return true, nil
    99  // 	}
   100  
   101  // 	// At this point we can conclude that both p and g exist.
   102  // 	//	Can't hurt to test that assertion.
   103  // 	if (!pexists) && (!gexists) {
   104  // 		return false, fmt.Errorf("Assertion failed. p and g should exist: pn=%q", pname)
   105  // 	}
   106  
   107  // 	pmodtime := pstat.ModTime()
   108  // 	gmodtime := gstat.ModTime()
   109  // 	// if plainfile < .gpg: unchanged
   110  // 	if pmodtime.Before(gmodtime) {
   111  // 		return false, nil
   112  // 	}
   113  // 	// if plainfile > .gpg: don't know, need to try diff
   114  // 	return false, fmt.Errorf("Can not know for sure. Try git diff?")
   115  // }
   116  
   117  func parseGroup(userinput string) (int, error) {
   118  	if userinput == "" {
   119  		return -1, fmt.Errorf("group spec is empty string")
   120  	}
   121  
   122  	// If it is a valid number, use it.
   123  	i, err := strconv.Atoi(userinput)
   124  	if err == nil {
   125  		return i, nil
   126  	}
   127  
   128  	// If not a number, look it up by name.
   129  	g, err := user.LookupGroup(userinput)
   130  	if err == nil {
   131  		i, err = strconv.Atoi(g.Gid)
   132  		return i, nil
   133  	}
   134  
   135  	// Give up.
   136  	return -1, err
   137  }
   138  
   139  // FindConfigDir tests various places until it finds the config dir.
   140  // If we can't determine the relative path, "" is returned.
   141  func FindConfigDir(reporoot, team string) (string, error) {
   142  
   143  	candidates := []string{}
   144  	if team != "" {
   145  		candidates = append(candidates, ".blackbox-"+team)
   146  	}
   147  	candidates = append(candidates, ".blackbox")
   148  	candidates = append(candidates, "keyrings/live")
   149  	logDebug.Printf("DEBUG: candidates = %q\n", candidates)
   150  
   151  	maxDirLevels := 30 // Prevent an infinite loop
   152  	relpath := "."
   153  	for i := 0; i < maxDirLevels; i++ {
   154  		// Does relpath contain any of our directory names?
   155  		for _, c := range candidates {
   156  			t := filepath.Join(relpath, c)
   157  			logDebug.Printf("Trying %q\n", t)
   158  			fi, err := os.Stat(t)
   159  			if err == nil && fi.IsDir() {
   160  				return t, nil
   161  			}
   162  			if err == nil {
   163  				return "", fmt.Errorf("path %q is not a directory: %w", t, err)
   164  			}
   165  			if !os.IsNotExist(err) {
   166  				return "", fmt.Errorf("dirExists access error: %w", err)
   167  			}
   168  		}
   169  
   170  		// If we are at the root, stop.
   171  		if abs, _ := filepath.Abs(relpath); abs == "/" {
   172  			break
   173  		}
   174  		// Try one directory up
   175  		relpath = filepath.Join("..", relpath)
   176  	}
   177  
   178  	return "", fmt.Errorf("No .blackbox (or equiv) directory found")
   179  }
   180  
   181  func gpgAgentNotice() {
   182  	// Is gpg-agent configured?
   183  	if os.Getenv("GPG_AGENT_INFO") != "" {
   184  		return
   185  	}
   186  	// Are we on macOS?
   187  	if runtime.GOOS == "darwin" {
   188  		// We assume the use of https://gpgtools.org, which
   189  		// uses the keychain.
   190  		return
   191  	}
   192  
   193  	// TODO(tlim): v1 verifies that "gpg-agent --version" outputs a version
   194  	// string that is 2.1.0 or higher.  It seems that 1.x is incompatible.
   195  
   196  	fmt.Println("WARNING: You probably want to run gpg-agent as")
   197  	fmt.Println("you will be asked for your passphrase many times.")
   198  	fmt.Println("Example: $ eval $(gpg-agent --daemon)")
   199  	fmt.Print("Press CTRL-C now to stop. ENTER to continue: ")
   200  	input := bufio.NewScanner(os.Stdin)
   201  	input.Scan()
   202  }
   203  
   204  func shouldWeOverwrite() {
   205  	fmt.Println()
   206  	fmt.Println("WARNING: This will overwrite any unencrypted files laying about.")
   207  	fmt.Print("Press CTRL-C now to stop. ENTER to continue: ")
   208  	input := bufio.NewScanner(os.Stdin)
   209  	input.Scan()
   210  }
   211  
   212  // PrettyCommitMessage generates a pretty commit message.
   213  func PrettyCommitMessage(verb string, files []string) string {
   214  	if len(files) == 0 {
   215  		// This use-case should probably be an error.
   216  		return verb + " (no files)"
   217  	}
   218  	rfiles := makesafe.RedactMany(files)
   219  	m, truncated := makesafe.FirstFewFlag(rfiles)
   220  	if truncated {
   221  		return verb + ": " + m
   222  	}
   223  	return verb + ": " + m
   224  }