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

     1  package box
     2  
     3  // This file implements the business logic related to a black box.
     4  // These functions are usually called from cmd/blackbox/drive.go or
     5  // external sytems that use box as a module.
     6  import (
     7  	"bufio"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"os"
    11  	"path/filepath"
    12  	"sort"
    13  	"strconv"
    14  	"strings"
    15  
    16  	"github.com/StackExchange/blackbox/v2/pkg/bbutil"
    17  	"github.com/StackExchange/blackbox/v2/pkg/makesafe"
    18  	"github.com/olekukonko/tablewriter"
    19  )
    20  
    21  // AdminAdd adds admins.
    22  func (bx *Box) AdminAdd(nom string, sdir string) error {
    23  	err := bx.getAdmins()
    24  	if err != nil {
    25  		return err
    26  	}
    27  
    28  	//fmt.Printf("ADMINS=%q\n", bx.Admins)
    29  
    30  	// Check for duplicates.
    31  	if i := sort.SearchStrings(bx.Admins, nom); i < len(bx.Admins) && bx.Admins[i] == nom {
    32  		return fmt.Errorf("Admin %v already an admin", nom)
    33  	}
    34  
    35  	bx.logDebug.Printf("ADMIN ADD rbd=%q\n", bx.RepoBaseDir)
    36  	changedFiles, err := bx.Crypter.AddNewKey(nom, bx.RepoBaseDir, sdir, bx.ConfigPath)
    37  	if err != nil {
    38  		return fmt.Errorf("AdminAdd failed AddNewKey: %v", err)
    39  	}
    40  
    41  	// TODO(tlim): Try the json file.
    42  
    43  	// Try the legacy file:
    44  	fn := filepath.Join(bx.ConfigPath, "blackbox-admins.txt")
    45  	bx.logDebug.Printf("Admins file: %q", fn)
    46  	err = bbutil.AddLinesToSortedFile(fn, nom)
    47  	if err != nil {
    48  		return fmt.Errorf("could not update file (%q,%q): %v", fn, nom, err)
    49  	}
    50  	changedFiles = append([]string{fn}, changedFiles...)
    51  
    52  	bx.Vcs.NeedsCommit("NEW ADMIN: "+nom, bx.RepoBaseDir, changedFiles)
    53  	return nil
    54  }
    55  
    56  // AdminList lists the admin id's.
    57  func (bx *Box) AdminList() error {
    58  	err := bx.getAdmins()
    59  	if err != nil {
    60  		return err
    61  	}
    62  
    63  	for _, v := range bx.Admins {
    64  		fmt.Println(v)
    65  	}
    66  	return nil
    67  }
    68  
    69  // AdminRemove removes an id from the admin list.
    70  func (bx *Box) AdminRemove([]string) error {
    71  	return fmt.Errorf("NOT IMPLEMENTED: AdminRemove")
    72  }
    73  
    74  // Cat outputs a file, unencrypting if needed.
    75  func (bx *Box) Cat(names []string) error {
    76  	if err := anyGpg(names); err != nil {
    77  		return fmt.Errorf("cat: %w", err)
    78  	}
    79  
    80  	err := bx.getFiles()
    81  	if err != nil {
    82  		return err
    83  	}
    84  
    85  	for _, name := range names {
    86  		var out []byte
    87  		var err error
    88  		if _, ok := bx.FilesSet[name]; ok {
    89  			out, err = bx.Crypter.Cat(name)
    90  		} else {
    91  			out, err = ioutil.ReadFile(name)
    92  		}
    93  		if err != nil {
    94  			bx.logErr.Printf("BX_CRY3\n")
    95  			return fmt.Errorf("cat: %w", err)
    96  		}
    97  		fmt.Print(string(out))
    98  	}
    99  	return nil
   100  }
   101  
   102  // Decrypt decrypts a file.
   103  func (bx *Box) Decrypt(names []string, overwrite bool, bulkpause bool, setgroup string) error {
   104  	var err error
   105  
   106  	if err := anyGpg(names); err != nil {
   107  		return err
   108  	}
   109  
   110  	err = bx.getFiles()
   111  	if err != nil {
   112  		return err
   113  	}
   114  
   115  	if bulkpause {
   116  		gpgAgentNotice()
   117  	}
   118  
   119  	groupchange := false
   120  	gid := -1
   121  	if setgroup != "" {
   122  		gid, err = parseGroup(setgroup)
   123  		if err != nil {
   124  			return fmt.Errorf("Invalid group name or gid: %w", err)
   125  		}
   126  		groupchange = true
   127  	}
   128  	bx.logDebug.Printf("DECRYPT GROUP %q %v,%v\n", setgroup, groupchange, gid)
   129  
   130  	if len(names) == 0 {
   131  		names = bx.Files
   132  	}
   133  	return decryptMany(bx, names, overwrite, groupchange, gid)
   134  }
   135  
   136  func decryptMany(bx *Box, names []string, overwrite bool, groupchange bool, gid int) error {
   137  
   138  	// TODO(tlim): If we want to decrypt them in parallel, go has a helper function
   139  	// called "sync.WaitGroup()"" which would be useful here.  We would probably
   140  	// want to add a flag on the command line (stored in a field such as bx.ParallelMax)
   141  	// that limits the amount of parallelism. The default for the flag should
   142  	// probably be runtime.NumCPU().
   143  
   144  	for _, name := range names {
   145  		fmt.Printf("========== DECRYPTING %q\n", name)
   146  		if !bx.FilesSet[name] {
   147  			bx.logErr.Printf("Skipping %q: File not registered with Blackbox", name)
   148  			continue
   149  		}
   150  		if (!overwrite) && bbutil.FileExistsOrProblem(name) {
   151  			bx.logErr.Printf("Skipping %q: Will not overwrite existing file", name)
   152  			continue
   153  		}
   154  
   155  		// TODO(tlim) v1 detects zero-length files and removes them, even
   156  		// if overwrite is disabled. I don't think anyone has ever used that
   157  		// feature. That said, if we want to do that, we would implement it here.
   158  
   159  		// TODO(tlim) v1 takes the md5 hash of the plaintext before it decrypts,
   160  		// then compares the new plaintext's md5. It prints "EXTRACTED" if
   161  		// there is a change.
   162  
   163  		err := bx.Crypter.Decrypt(name, bx.Umask, overwrite)
   164  		if err != nil {
   165  			bx.logErr.Printf("%q: %v", name, err)
   166  			continue
   167  		}
   168  
   169  		// FIXME(tlim): Clone the file perms from the .gpg file to the plaintext file.
   170  
   171  		if groupchange {
   172  			// FIXME(tlim): Also "chmod g+r" the file.
   173  			os.Chown(name, -1, gid)
   174  		}
   175  	}
   176  	return nil
   177  }
   178  
   179  // Diff ...
   180  func (bx *Box) Diff([]string) error {
   181  	return fmt.Errorf("NOT IMPLEMENTED: Diff")
   182  }
   183  
   184  // Edit unencrypts, calls editor, calls encrypt.
   185  func (bx *Box) Edit(names []string) error {
   186  
   187  	if err := anyGpg(names); err != nil {
   188  		return err
   189  	}
   190  
   191  	err := bx.getFiles()
   192  	if err != nil {
   193  		return err
   194  	}
   195  
   196  	for _, name := range names {
   197  		if _, ok := bx.FilesSet[name]; ok {
   198  			if !bbutil.FileExistsOrProblem(name) {
   199  				err := bx.Crypter.Decrypt(name, bx.Umask, false)
   200  				if err != nil {
   201  					return fmt.Errorf("edit failed %q: %w", name, err)
   202  				}
   203  			}
   204  		}
   205  		err := bbutil.RunBash(bx.Editor, name)
   206  		if err != nil {
   207  			return err
   208  		}
   209  	}
   210  	return nil
   211  }
   212  
   213  // Encrypt encrypts a file.
   214  func (bx *Box) Encrypt(names []string, shred bool) error {
   215  	var err error
   216  
   217  	if err = anyGpg(names); err != nil {
   218  		return err
   219  	}
   220  
   221  	err = bx.getAdmins()
   222  	if err != nil {
   223  		return err
   224  	}
   225  
   226  	err = bx.getFiles()
   227  	if err != nil {
   228  		return err
   229  	}
   230  	if len(names) == 0 {
   231  		names = bx.Files
   232  	}
   233  
   234  	enames, err := encryptMany(bx, names, shred)
   235  
   236  	bx.Vcs.NeedsCommit(
   237  		PrettyCommitMessage("ENCRYPTED", names),
   238  		bx.RepoBaseDir,
   239  		enames,
   240  	)
   241  
   242  	return err
   243  }
   244  
   245  func encryptMany(bx *Box, names []string, shred bool) ([]string, error) {
   246  	var enames []string
   247  	for _, name := range names {
   248  		fmt.Printf("========== ENCRYPTING %q\n", name)
   249  		if !bx.FilesSet[name] {
   250  			bx.logErr.Printf("Skipping %q: File not registered with Blackbox", name)
   251  			continue
   252  		}
   253  		if !bbutil.FileExistsOrProblem(name) {
   254  			bx.logErr.Printf("Skipping. Plaintext does not exist: %q", name)
   255  			continue
   256  		}
   257  		ename, err := bx.Crypter.Encrypt(name, bx.Umask, bx.Admins)
   258  		if err != nil {
   259  			bx.logErr.Printf("Failed to encrypt %q: %v", name, err)
   260  			continue
   261  		}
   262  		enames = append(enames, ename)
   263  		if shred {
   264  			bx.Shred([]string{name})
   265  		}
   266  	}
   267  
   268  	return enames, nil
   269  }
   270  
   271  // FileAdd enrolls files.
   272  func (bx *Box) FileAdd(names []string, shred bool) error {
   273  	bx.logDebug.Printf("FileAdd(shred=%v, %v)", shred, names)
   274  
   275  	// Check for dups.
   276  	// Encrypt them all.
   277  	// If that succeeds, add to the blackbox-files.txt file.
   278  	// (optionally) shred the plaintext.
   279  
   280  	// FIXME(tlim): Check if the plaintext is in GIT.  If it is,
   281  	// remove it from Git and print a warning that they should
   282  	// eliminate the history or rotate any secrets.
   283  
   284  	if err := anyGpg(names); err != nil {
   285  		return err
   286  	}
   287  
   288  	err := bx.getAdmins()
   289  	if err != nil {
   290  		return err
   291  	}
   292  	err = bx.getFiles()
   293  	if err != nil {
   294  		return err
   295  	}
   296  	if err := anyGpg(names); err != nil {
   297  		return err
   298  	}
   299  
   300  	// Check for newlines
   301  	for _, n := range names {
   302  		if strings.ContainsAny(n, "\n") {
   303  			return fmt.Errorf("file %q contains a newlineregistered", n)
   304  		}
   305  	}
   306  
   307  	// Check for duplicates.
   308  	for _, n := range names {
   309  		if i := sort.SearchStrings(bx.Files, n); i < len(bx.Files) && bx.Files[i] == n {
   310  			return fmt.Errorf("file %q already registered", n)
   311  		}
   312  	}
   313  
   314  	// Encrypt
   315  	var needsCommit []string
   316  	for _, name := range names {
   317  		s, err := bx.Crypter.Encrypt(name, bx.Umask, bx.Admins)
   318  		if err != nil {
   319  			return fmt.Errorf("AdminAdd failed AddNewKey: %v", err)
   320  		}
   321  		needsCommit = append(needsCommit, s)
   322  	}
   323  
   324  	// TODO(tlim): Try the json file.
   325  
   326  	// Try the legacy file:
   327  	fn := filepath.Join(bx.ConfigPath, "blackbox-files.txt")
   328  	bx.logDebug.Printf("Files file: %q", fn)
   329  	err = bbutil.AddLinesToSortedFile(fn, names...)
   330  	if err != nil {
   331  		return fmt.Errorf("could not update file (%q,%q): %v", fn, names, err)
   332  	}
   333  
   334  	err = bx.Shred(names)
   335  	if err != nil {
   336  		bx.logErr.Printf("Error while shredding: %v", err)
   337  	}
   338  
   339  	bx.Vcs.CommitTitle("BLACKBOX ADD FILE: " + makesafe.FirstFew(makesafe.ShellMany(names)))
   340  
   341  	bx.Vcs.IgnoreFiles(bx.RepoBaseDir, names)
   342  
   343  	bx.Vcs.NeedsCommit(
   344  		PrettyCommitMessage("blackbox-files.txt add", names),
   345  		bx.RepoBaseDir,
   346  		append([]string{filepath.Join(bx.ConfigPath, "blackbox-files.txt")}, needsCommit...),
   347  	)
   348  	return nil
   349  }
   350  
   351  // FileList lists the files.
   352  func (bx *Box) FileList() error {
   353  	err := bx.getFiles()
   354  	if err != nil {
   355  		return err
   356  	}
   357  	for _, v := range bx.Files {
   358  		fmt.Println(v)
   359  	}
   360  	return nil
   361  }
   362  
   363  // FileRemove de-enrolls files.
   364  func (bx *Box) FileRemove(names []string) error {
   365  	return fmt.Errorf("NOT IMPLEMENTED: FileRemove")
   366  }
   367  
   368  // Info prints debugging info.
   369  func (bx *Box) Info() error {
   370  
   371  	err := bx.getFiles()
   372  	if err != nil {
   373  		bx.logErr.Printf("Info getFiles: %v", err)
   374  	}
   375  
   376  	err = bx.getAdmins()
   377  	if err != nil {
   378  		bx.logErr.Printf("Info getAdmins: %v", err)
   379  	}
   380  
   381  	fmt.Println("BLACKBOX:")
   382  	fmt.Printf("          Debug: %v\n", bx.Debug)
   383  	fmt.Printf("           Team: %q\n", bx.Team)
   384  	fmt.Printf("    RepoBaseDir: %q\n", bx.RepoBaseDir)
   385  	fmt.Printf("     ConfigPath: %q\n", bx.ConfigPath)
   386  	fmt.Printf("          Umask: %04o\n", bx.Umask)
   387  	fmt.Printf("         Editor: %v\n", bx.Editor)
   388  	fmt.Printf("       Shredder: %v\n", bbutil.ShredInfo())
   389  	fmt.Printf("         Admins: count=%v\n", len(bx.Admins))
   390  	fmt.Printf("          Files: count=%v\n", len(bx.Files))
   391  	fmt.Printf("       FilesSet: count=%v\n", len(bx.FilesSet))
   392  	fmt.Printf("            Vcs: %v\n", bx.Vcs)
   393  	fmt.Printf("        VcsName: %q\n", bx.Vcs.Name())
   394  	fmt.Printf("        Crypter: %v\n", bx.Crypter)
   395  	fmt.Printf("    CrypterName: %q\n", bx.Crypter.Name())
   396  
   397  	return nil
   398  }
   399  
   400  // Init initializes a repo.
   401  func (bx *Box) Init(yes, vcsname string) error {
   402  	fmt.Printf("VCS root is: %q\n", bx.RepoBaseDir)
   403  
   404  	fmt.Printf("team is: %q\n", bx.Team)
   405  	fmt.Printf("configdir will be: %q\n", bx.ConfigPath)
   406  
   407  	if yes != "yes" {
   408  		fmt.Printf("Enable blackbox for this %v repo? (yes/no)? ", bx.Vcs.Name())
   409  		input := bufio.NewScanner(os.Stdin)
   410  		input.Scan()
   411  		ans := input.Text()
   412  		b, err := strconv.ParseBool(ans)
   413  		if err != nil {
   414  			b = false
   415  			if len(ans) > 0 {
   416  				if ans[0] == 'y' || ans[0] == 'Y' {
   417  					b = true
   418  				}
   419  			}
   420  		}
   421  		if !b {
   422  			fmt.Println("Ok. Maybe some other time.")
   423  			return nil
   424  		}
   425  	}
   426  
   427  	err := os.Mkdir(bx.ConfigPath, 0o750)
   428  	if err != nil {
   429  		return err
   430  	}
   431  
   432  	ba := filepath.Join(bx.ConfigPath, "blackbox-admins.txt")
   433  	bf := filepath.Join(bx.ConfigPath, "blackbox-files.txt")
   434  	bbutil.Touch(ba)
   435  	bbutil.Touch(bf)
   436  	bx.Vcs.SetFileTypeUnix(bx.RepoBaseDir, ba, bf)
   437  
   438  	bx.Vcs.IgnoreAnywhere(bx.RepoBaseDir, []string{
   439  		"pubring.gpg~",
   440  		"pubring.kbx~",
   441  		"secring.gpg",
   442  	})
   443  
   444  	fs := []string{ba, bf}
   445  	bx.Vcs.NeedsCommit(
   446  		"NEW: "+strings.Join(makesafe.RedactMany(fs), " "),
   447  		bx.RepoBaseDir,
   448  		fs,
   449  	)
   450  
   451  	bx.Vcs.CommitTitle("INITIALIZE BLACKBOX")
   452  	return nil
   453  }
   454  
   455  // Reencrypt decrypts and reencrypts files.
   456  func (bx *Box) Reencrypt(names []string, overwrite bool, bulkpause bool) error {
   457  
   458  	allFiles := false
   459  
   460  	if err := anyGpg(names); err != nil {
   461  		return err
   462  	}
   463  	if err := bx.getAdmins(); err != nil {
   464  		return err
   465  	}
   466  	if err := bx.getFiles(); err != nil {
   467  		return err
   468  	}
   469  	if len(names) == 0 {
   470  		names = bx.Files
   471  		allFiles = true
   472  	}
   473  
   474  	if bulkpause {
   475  		gpgAgentNotice()
   476  	}
   477  
   478  	fmt.Println("========== blackbox administrators are:")
   479  	bx.AdminList()
   480  	fmt.Println("========== (the above people will be able to access the file)")
   481  
   482  	if overwrite {
   483  		bbutil.ShredFiles(names)
   484  	} else {
   485  		warned := false
   486  		for _, n := range names {
   487  			if bbutil.FileExistsOrProblem(n) {
   488  				if !warned {
   489  					fmt.Printf("========== Shred these files?\n")
   490  					warned = true
   491  				}
   492  				fmt.Println("SHRED?", n)
   493  			}
   494  		}
   495  		if warned {
   496  			shouldWeOverwrite()
   497  		}
   498  	}
   499  
   500  	// Decrypt
   501  	if err := decryptMany(bx, names, overwrite, false, 0); err != nil {
   502  		return fmt.Errorf("reencrypt failed decrypt: %w", err)
   503  	}
   504  	enames, err := encryptMany(bx, names, false)
   505  	if err != nil {
   506  		return fmt.Errorf("reencrypt failed encrypt: %w", err)
   507  	}
   508  	if err := bbutil.ShredFiles(names); err != nil {
   509  		return fmt.Errorf("reencrypt failed shred: %w", err)
   510  	}
   511  
   512  	if allFiles {
   513  		// If the "--all" flag was used, don't try to list all the files.
   514  		bx.Vcs.NeedsCommit(
   515  			"REENCRYPT all files",
   516  			bx.RepoBaseDir,
   517  			enames,
   518  		)
   519  	} else {
   520  		bx.Vcs.NeedsCommit(
   521  			PrettyCommitMessage("REENCRYPT", names),
   522  			bx.RepoBaseDir,
   523  			enames,
   524  		)
   525  
   526  	}
   527  
   528  	return nil
   529  }
   530  
   531  // Shred shreds files.
   532  func (bx *Box) Shred(names []string) error {
   533  
   534  	if err := anyGpg(names); err != nil {
   535  		return err
   536  	}
   537  
   538  	err := bx.getFiles()
   539  	// Calling getFiles() has the benefit of making sure we are in a repo.
   540  	if err != nil {
   541  		return err
   542  	}
   543  
   544  	if len(names) == 0 {
   545  		names = bx.Files
   546  	}
   547  
   548  	return bbutil.ShredFiles(names)
   549  }
   550  
   551  // Status prints the status of files.
   552  func (bx *Box) Status(names []string, nameOnly bool, match string) error {
   553  
   554  	err := bx.getFiles()
   555  	if err != nil {
   556  		return err
   557  	}
   558  
   559  	var flist []string
   560  	if len(names) == 0 {
   561  		flist = bx.Files
   562  	} else {
   563  		flist = names
   564  	}
   565  
   566  	var data [][]string
   567  	var onlylist []string
   568  	thirdColumn := false
   569  	var tcData bool
   570  
   571  	for _, name := range flist {
   572  		var stat string
   573  		var err error
   574  		if _, ok := bx.FilesSet[name]; ok {
   575  			stat, err = FileStatus(name)
   576  		} else {
   577  			stat, err = "NOTREG", nil
   578  		}
   579  		if (match == "") || (stat == match) {
   580  			if err == nil {
   581  				data = append(data, []string{stat, name})
   582  				onlylist = append(onlylist, name)
   583  			} else {
   584  				thirdColumn = tcData
   585  				data = append(data, []string{stat, name, fmt.Sprintf("%v", err)})
   586  				onlylist = append(onlylist, fmt.Sprintf("%v: %v", name, err))
   587  			}
   588  		}
   589  	}
   590  
   591  	if nameOnly {
   592  		fmt.Println(strings.Join(onlylist, "\n"))
   593  		return nil
   594  	}
   595  
   596  	table := tablewriter.NewWriter(os.Stdout)
   597  	table.SetAutoWrapText(false)
   598  	if thirdColumn {
   599  		table.SetHeader([]string{"Status", "Name", "Error"})
   600  	} else {
   601  		table.SetHeader([]string{"Status", "Name"})
   602  	}
   603  	for _, v := range data {
   604  		table.Append(v)
   605  	}
   606  	table.Render() // Send output
   607  
   608  	return nil
   609  }
   610  
   611  // TestingInitRepo initializes a repo.
   612  // Uses bx.Vcs to create ".git" or whatever.
   613  // Uses bx.Vcs to discover what was created, testing its work.
   614  func (bx *Box) TestingInitRepo() error {
   615  
   616  	if bx.Vcs == nil {
   617  		fmt.Println("bx.Vcs is nil")
   618  		fmt.Printf("BLACKBOX_VCS=%q\n", os.Getenv("BLACKBOX_VCS"))
   619  		os.Exit(1)
   620  	}
   621  	fmt.Printf("ABOUT TO CALL TestingInitRepo\n")
   622  	fmt.Printf("vcs = %v\n", bx.Vcs.Name())
   623  	err := bx.Vcs.TestingInitRepo()
   624  	fmt.Printf("RETURNED from TestingInitRepo: %v\n", err)
   625  	fmt.Println(os.Getwd())
   626  	if err != nil {
   627  		return fmt.Errorf("TestingInitRepo returned: %w", err)
   628  	}
   629  	if b, _ := bx.Vcs.Discover(); !b {
   630  		return fmt.Errorf("TestingInitRepo failed Discovery")
   631  	}
   632  	return nil
   633  }