Implement blackbox in Golang (#250)

* Initial release
This commit is contained in:
Tom Limoncelli
2020-07-24 14:21:33 -04:00
committed by GitHub
parent e049c02655
commit 1c77c87555
86 changed files with 6074 additions and 22 deletions

233
pkg/box/box.go Normal file
View File

@@ -0,0 +1,233 @@
package box
// box implements the box model.
import (
"fmt"
"log"
"os"
"path/filepath"
"sort"
"strings"
"github.com/StackExchange/blackbox/v2/pkg/bblog"
"github.com/StackExchange/blackbox/v2/pkg/bbutil"
"github.com/StackExchange/blackbox/v2/pkg/crypters"
"github.com/StackExchange/blackbox/v2/pkg/vcs"
"github.com/urfave/cli/v2"
)
var logErr *log.Logger
var logDebug *log.Logger
// Box describes what we know about a box.
type Box struct {
// Paths:
Team string // Name of the team (i.e. .blackbox-$TEAM)
RepoBaseDir string // Rel path to the VCS repo.
ConfigPath string // Abs or Rel path to the .blackbox (or whatever) directory.
ConfigRO bool // True if we should not try to change files in ConfigPath.
// Settings:
Umask int // umask to set when decrypting
Editor string // Editor to call
Debug bool // Are we in debug logging mode?
// Cache of data gathered from .blackbox:
Admins []string // If non-empty, the list of admins.
Files []string // If non-empty, the list of files.
FilesSet map[string]bool // If non-nil, a set of Files.
// Handles to interfaces:
Vcs vcs.Vcs // Interface access to the VCS.
Crypter crypters.Crypter // Inteface access to GPG.
logErr *log.Logger
logDebug *log.Logger
}
// StatusMode is a type of query.
type StatusMode int
const (
// Itemized is blah
Itemized StatusMode = iota // Individual files by name
// All files is blah
All
// Unchanged is blah
Unchanged
// Changed is blah
Changed
)
// NewFromFlags creates a box using items from flags. Nearly all subcommands use this.
func NewFromFlags(c *cli.Context) *Box {
// The goal of this is to create a fully-populated box (and box.Vcs)
// so that all subcommands have all the fields and interfaces they need
// to do their job.
logErr = bblog.GetErr()
logDebug = bblog.GetDebug(c.Bool("debug"))
bx := &Box{
Umask: c.Int("umask"),
Editor: c.String("editor"),
Team: c.String("team"),
logErr: bblog.GetErr(),
logDebug: bblog.GetDebug(c.Bool("debug")),
Debug: c.Bool("debug"),
}
// Discover which kind of VCS is in use, and the repo root.
bx.Vcs, bx.RepoBaseDir = vcs.Discover()
// Discover the crypto backend (GnuPG, go-openpgp, etc.)
bx.Crypter = crypters.SearchByName(c.String("crypto"), c.Bool("debug"))
if bx.Crypter == nil {
fmt.Printf("ERROR! No CRYPTER found! Please set --crypto correctly or use the damn default\n")
os.Exit(1)
}
// Find the .blackbox (or equiv.) directory.
var err error
configFlag := c.String("config")
if configFlag != "" {
// Flag is set. Better make sure it is valid.
if !filepath.IsAbs(configFlag) {
fmt.Printf("config flag value is a relative path. Too risky. Exiting.\n")
os.Exit(1)
// NB(tlim): We could return filepath.Abs(config) or maybe it just
// works as is. I don't know, and until we have a use case to prove
// it out, it's best to just not implement this.
}
bx.ConfigPath = configFlag
bx.ConfigRO = true // External configs treated as read-only.
// TODO(tlim): We could get fancy here and set ConfigReadOnly=true only
// if we are sure configFlag is not within bx.RepoBaseDir. Again, I'd
// like to see a use-case before we implement this.
return bx
}
// Normal path. Flag not set, so we discover the path.
bx.ConfigPath, err = FindConfigDir(bx.RepoBaseDir, c.String("team"))
if err != nil && c.Command.Name != "info" {
fmt.Printf("Can't find .blackbox or equiv. Have you run init?\n")
os.Exit(1)
}
return bx
}
// NewUninitialized creates a box in a pre-init situation.
func NewUninitialized(c *cli.Context) *Box {
/*
This is for "blackbox init" (used before ".blackbox*" exists)
Init needs: How we populate it:
bx.Vcs: Discovered by calling each plug-in until succeeds.
bx.ConfigDir: Generated algorithmically (it doesn't exist yet).
*/
bx := &Box{
Umask: c.Int("umask"),
Editor: c.String("editor"),
Team: c.String("team"),
logErr: bblog.GetErr(),
logDebug: bblog.GetDebug(c.Bool("debug")),
Debug: c.Bool("debug"),
}
bx.Vcs, bx.RepoBaseDir = vcs.Discover()
if c.String("configdir") == "" {
rel := ".blackbox"
if bx.Team != "" {
rel = ".blackbox-" + bx.Team
}
bx.ConfigPath = filepath.Join(bx.RepoBaseDir, rel)
} else {
// Wait. The user is using the --config flag on a repo that
// hasn't been created yet? I hope this works!
fmt.Printf("ERROR: You can not set --config when initializing a new repo. Please run this command from within a repo, with no --config flag. Or, file a bug explaining your use caseyour use-case. Exiting!\n")
os.Exit(1)
// TODO(tlim): We could get fancy here and query the Vcs to see if the
// path would fall within the repo, figure out the relative path, and
// use that value. (and error if configflag is not within the repo).
// That would be error prone and would only help the zero users that
// ever see the above error message.
}
return bx
}
// NewForTestingInit creates a box in a bare environment.
func NewForTestingInit(vcsname string) *Box {
/*
This is for "blackbox test_init" (secret command used in integration tests; when nothing exists)
TestingInitRepo only uses bx.Vcs, so that's all we set.
Populates bx.Vcs by finding the provider named vcsname.
*/
bx := &Box{}
// Find the
var vh vcs.Vcs
var err error
vcsname = strings.ToLower(vcsname)
for _, v := range vcs.Catalog {
if strings.ToLower(v.Name) == vcsname {
vh, err = v.New()
if err != nil {
return nil // No idea how that would happen.
}
}
}
bx.Vcs = vh
return bx
}
func (bx *Box) getAdmins() error {
// Memoized
if len(bx.Admins) != 0 {
return nil
}
// TODO(tlim): Try the json file.
// Try the legacy file:
fn := filepath.Join(bx.ConfigPath, "blackbox-admins.txt")
bx.logDebug.Printf("Admins file: %q", fn)
a, err := bbutil.ReadFileLines(fn)
if err != nil {
return fmt.Errorf("getAdmins can't load %q: %v", fn, err)
}
if !sort.StringsAreSorted(a) {
return fmt.Errorf("file corrupt. Lines not sorted: %v", fn)
}
bx.Admins = a
return nil
}
// getFiles populates Files and FileMap.
func (bx *Box) getFiles() error {
if len(bx.Files) != 0 {
return nil
}
// TODO(tlim): Try the json file.
// Try the legacy file:
fn := filepath.Join(bx.ConfigPath, "blackbox-files.txt")
bx.logDebug.Printf("Files file: %q", fn)
a, err := bbutil.ReadFileLines(fn)
if err != nil {
return fmt.Errorf("getFiles can't load %q: %v", fn, err)
}
if !sort.StringsAreSorted(a) {
return fmt.Errorf("file corrupt. Lines not sorted: %v", fn)
}
for _, n := range a {
bx.Files = append(bx.Files, filepath.Join(bx.RepoBaseDir, n))
}
bx.FilesSet = make(map[string]bool, len(bx.Files))
for _, s := range bx.Files {
bx.FilesSet[s] = true
}
return nil
}

224
pkg/box/boxutils.go Normal file
View File

@@ -0,0 +1,224 @@
package box
import (
"bufio"
"fmt"
"os"
"os/user"
"path/filepath"
"runtime"
"strconv"
"strings"
"github.com/StackExchange/blackbox/v2/pkg/makesafe"
)
// FileStatus returns the status of a file.
func FileStatus(name string) (string, error) {
/*
DECRYPTED: File is decrypted and ready to edit (unknown if it has been edited).
ENCRYPTED: GPG file is newer than plaintext. Indicates recented edited then encrypted.
SHREDDED: Plaintext is missing.
GPGMISSING: The .gpg file is missing. Oops?
PLAINERROR: Can't access the plaintext file to determine status.
GPGERROR: Can't access .gpg file to determine status.
*/
p := name
e := p + ".gpg"
ps, perr := os.Stat(p)
es, eerr := os.Stat(e)
if perr == nil && eerr == nil {
if ps.ModTime().Before(es.ModTime()) {
return "ENCRYPTED", nil
}
return "DECRYPTED", nil
}
if os.IsNotExist(perr) && os.IsNotExist(eerr) {
return "BOTHMISSING", nil
}
if eerr != nil {
if os.IsNotExist(eerr) {
return "GPGMISSING", nil
}
return "GPGERROR", eerr
}
if perr != nil {
if os.IsNotExist(perr) {
return "SHREDDED", nil
}
}
return "PLAINERROR", perr
}
func anyGpg(names []string) error {
for _, name := range names {
if strings.HasSuffix(name, ".gpg") {
return fmt.Errorf(
"no not specify .gpg files. Specify %q not %q",
strings.TrimSuffix(name, ".gpg"), name)
}
}
return nil
}
// func isChanged(pname string) (bool, error) {
// // if .gpg exists but not plainfile: unchanged
// // if plaintext exists but not .gpg: changed
// // if plainfile < .gpg: unchanged
// // if plainfile > .gpg: don't know, need to try diff
// // Gather info about the files:
// pstat, perr := os.Stat(pname)
// if perr != nil && (!os.IsNotExist(perr)) {
// return false, fmt.Errorf("isChanged(%q) returned error: %w", pname, perr)
// }
// gname := pname + ".gpg"
// gstat, gerr := os.Stat(gname)
// if gerr != nil && (!os.IsNotExist(perr)) {
// return false, fmt.Errorf("isChanged(%q) returned error: %w", gname, gerr)
// }
// pexists := perr == nil
// gexists := gerr == nil
// // Use the above rules:
// // if .gpg exists but not plainfile: unchanged
// if gexists && !pexists {
// return false, nil
// }
// // if plaintext exists but not .gpg: changed
// if pexists && !gexists {
// return true, nil
// }
// // At this point we can conclude that both p and g exist.
// // Can't hurt to test that assertion.
// if (!pexists) && (!gexists) {
// return false, fmt.Errorf("Assertion failed. p and g should exist: pn=%q", pname)
// }
// pmodtime := pstat.ModTime()
// gmodtime := gstat.ModTime()
// // if plainfile < .gpg: unchanged
// if pmodtime.Before(gmodtime) {
// return false, nil
// }
// // if plainfile > .gpg: don't know, need to try diff
// return false, fmt.Errorf("Can not know for sure. Try git diff?")
// }
func parseGroup(userinput string) (int, error) {
if userinput == "" {
return -1, fmt.Errorf("group spec is empty string")
}
// If it is a valid number, use it.
i, err := strconv.Atoi(userinput)
if err == nil {
return i, nil
}
// If not a number, look it up by name.
g, err := user.LookupGroup(userinput)
if err == nil {
i, err = strconv.Atoi(g.Gid)
return i, nil
}
// Give up.
return -1, err
}
// FindConfigDir tests various places until it finds the config dir.
// If we can't determine the relative path, "" is returned.
func FindConfigDir(reporoot, team string) (string, error) {
candidates := []string{}
if team != "" {
candidates = append(candidates, ".blackbox-"+team)
}
candidates = append(candidates, ".blackbox")
candidates = append(candidates, "keyrings/live")
logDebug.Printf("DEBUG: candidates = %q\n", candidates)
maxDirLevels := 30 // Prevent an infinite loop
relpath := "."
for i := 0; i < maxDirLevels; i++ {
// Does relpath contain any of our directory names?
for _, c := range candidates {
t := filepath.Join(relpath, c)
logDebug.Printf("Trying %q\n", t)
fi, err := os.Stat(t)
if err == nil && fi.IsDir() {
return t, nil
}
if err == nil {
return "", fmt.Errorf("path %q is not a directory: %w", t, err)
}
if !os.IsNotExist(err) {
return "", fmt.Errorf("dirExists access error: %w", err)
}
}
// If we are at the root, stop.
if abs, _ := filepath.Abs(relpath); abs == "/" {
break
}
// Try one directory up
relpath = filepath.Join("..", relpath)
}
return "", fmt.Errorf("No .blackbox (or equiv) directory found")
}
func gpgAgentNotice() {
// Is gpg-agent configured?
if os.Getenv("GPG_AGENT_INFO") != "" {
return
}
// Are we on macOS?
if runtime.GOOS == "darwin" {
// We assume the use of https://gpgtools.org, which
// uses the keychain.
return
}
// TODO(tlim): v1 verifies that "gpg-agent --version" outputs a version
// string that is 2.1.0 or higher. It seems that 1.x is incompatible.
fmt.Println("WARNING: You probably want to run gpg-agent as")
fmt.Println("you will be asked for your passphrase many times.")
fmt.Println("Example: $ eval $(gpg-agent --daemon)")
fmt.Print("Press CTRL-C now to stop. ENTER to continue: ")
input := bufio.NewScanner(os.Stdin)
input.Scan()
}
func shouldWeOverwrite() {
fmt.Println()
fmt.Println("WARNING: This will overwrite any unencrypted files laying about.")
fmt.Print("Press CTRL-C now to stop. ENTER to continue: ")
input := bufio.NewScanner(os.Stdin)
input.Scan()
}
// PrettyCommitMessage generates a pretty commit message.
func PrettyCommitMessage(verb string, files []string) string {
if len(files) == 0 {
// This use-case should probably be an error.
return verb + " (no files)"
}
rfiles := makesafe.RedactMany(files)
m, truncated := makesafe.FirstFewFlag(rfiles)
if truncated {
return verb + ": " + m
}
return verb + ": " + m
}

35
pkg/box/pretty_test.go Normal file
View File

@@ -0,0 +1,35 @@
package box
import "testing"
func TestPrettyCommitMessage(t *testing.T) {
long := "aVeryVeryLongLongLongStringStringString"
for i, test := range []struct {
data []string
expected string
}{
{[]string{}, `HEADING (no files)`},
{[]string{"one"}, `HEADING: one`},
{[]string{"one", "two"}, `HEADING: one two`},
{[]string{"one", "two", "three"}, `HEADING: one two three`},
{[]string{"one", "two", "three", "four"},
`HEADING: one two three four`},
{[]string{"one", "two", "three", "four", "five"},
`HEADING: one two three four five`},
{[]string{"has spaces.txt"}, `HEADING: "has spaces.txt"`},
{[]string{"two\n"}, `HEADING: "twoX"(redacted)`},
{[]string{"smile😁eyes"}, `HEADING: smile😁eyes`},
{[]string{"tab\ttab", "two very long strings.txt"},
`HEADING: "tabXtab"(redacted) "two very long strings.txt"`},
{[]string{long, long, long, long},
"HEADING: " + long + " " + long + " " + long + " " + long + " ... " + long + "\n " + long + "\n " + long + "\n " + long + "\n"},
} {
g := PrettyCommitMessage("HEADING", test.data)
if g == test.expected {
//t.Logf("%03d: PASSED files=%q\n", i, test.data)
t.Logf("%03d: PASSED", i)
} else {
t.Errorf("%03d: FAILED files==%q got=(%q) wanted=(%q)\n", i, test.data, g, test.expected)
}
}
}

633
pkg/box/verbs.go Normal file
View File

@@ -0,0 +1,633 @@
package box
// This file implements the business logic related to a black box.
// These functions are usually called from cmd/blackbox/drive.go or
// external sytems that use box as a module.
import (
"bufio"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/StackExchange/blackbox/v2/pkg/bbutil"
"github.com/StackExchange/blackbox/v2/pkg/makesafe"
"github.com/olekukonko/tablewriter"
)
// AdminAdd adds admins.
func (bx *Box) AdminAdd(nom string, sdir string) error {
err := bx.getAdmins()
if err != nil {
return err
}
//fmt.Printf("ADMINS=%q\n", bx.Admins)
// Check for duplicates.
if i := sort.SearchStrings(bx.Admins, nom); i < len(bx.Admins) && bx.Admins[i] == nom {
return fmt.Errorf("Admin %v already an admin", nom)
}
bx.logDebug.Printf("ADMIN ADD rbd=%q\n", bx.RepoBaseDir)
changedFiles, err := bx.Crypter.AddNewKey(nom, bx.RepoBaseDir, sdir, bx.ConfigPath)
if err != nil {
return fmt.Errorf("AdminAdd failed AddNewKey: %v", err)
}
// TODO(tlim): Try the json file.
// Try the legacy file:
fn := filepath.Join(bx.ConfigPath, "blackbox-admins.txt")
bx.logDebug.Printf("Admins file: %q", fn)
err = bbutil.AddLinesToSortedFile(fn, nom)
if err != nil {
return fmt.Errorf("could not update file (%q,%q): %v", fn, nom, err)
}
changedFiles = append([]string{fn}, changedFiles...)
bx.Vcs.NeedsCommit("NEW ADMIN: "+nom, bx.RepoBaseDir, changedFiles)
return nil
}
// AdminList lists the admin id's.
func (bx *Box) AdminList() error {
err := bx.getAdmins()
if err != nil {
return err
}
for _, v := range bx.Admins {
fmt.Println(v)
}
return nil
}
// AdminRemove removes an id from the admin list.
func (bx *Box) AdminRemove([]string) error {
return fmt.Errorf("NOT IMPLEMENTED: AdminRemove")
}
// Cat outputs a file, unencrypting if needed.
func (bx *Box) Cat(names []string) error {
if err := anyGpg(names); err != nil {
return fmt.Errorf("cat: %w", err)
}
err := bx.getFiles()
if err != nil {
return err
}
for _, name := range names {
var out []byte
var err error
if _, ok := bx.FilesSet[name]; ok {
out, err = bx.Crypter.Cat(name)
} else {
out, err = ioutil.ReadFile(name)
}
if err != nil {
bx.logErr.Printf("BX_CRY3\n")
return fmt.Errorf("cat: %w", err)
}
fmt.Print(string(out))
}
return nil
}
// Decrypt decrypts a file.
func (bx *Box) Decrypt(names []string, overwrite bool, bulkpause bool, setgroup string) error {
var err error
if err := anyGpg(names); err != nil {
return err
}
err = bx.getFiles()
if err != nil {
return err
}
if bulkpause {
gpgAgentNotice()
}
groupchange := false
gid := -1
if setgroup != "" {
gid, err = parseGroup(setgroup)
if err != nil {
return fmt.Errorf("Invalid group name or gid: %w", err)
}
groupchange = true
}
bx.logDebug.Printf("DECRYPT GROUP %q %v,%v\n", setgroup, groupchange, gid)
if len(names) == 0 {
names = bx.Files
}
return decryptMany(bx, names, overwrite, groupchange, gid)
}
func decryptMany(bx *Box, names []string, overwrite bool, groupchange bool, gid int) error {
// TODO(tlim): If we want to decrypt them in parallel, go has a helper function
// called "sync.WaitGroup()"" which would be useful here. We would probably
// want to add a flag on the command line (stored in a field such as bx.ParallelMax)
// that limits the amount of parallelism. The default for the flag should
// probably be runtime.NumCPU().
for _, name := range names {
fmt.Printf("========== DECRYPTING %q\n", name)
if !bx.FilesSet[name] {
bx.logErr.Printf("Skipping %q: File not registered with Blackbox", name)
continue
}
if (!overwrite) && bbutil.FileExistsOrProblem(name) {
bx.logErr.Printf("Skipping %q: Will not overwrite existing file", name)
continue
}
// TODO(tlim) v1 detects zero-length files and removes them, even
// if overwrite is disabled. I don't think anyone has ever used that
// feature. That said, if we want to do that, we would implement it here.
// TODO(tlim) v1 takes the md5 hash of the plaintext before it decrypts,
// then compares the new plaintext's md5. It prints "EXTRACTED" if
// there is a change.
err := bx.Crypter.Decrypt(name, bx.Umask, overwrite)
if err != nil {
bx.logErr.Printf("%q: %v", name, err)
continue
}
// FIXME(tlim): Clone the file perms from the .gpg file to the plaintext file.
if groupchange {
// FIXME(tlim): Also "chmod g+r" the file.
os.Chown(name, -1, gid)
}
}
return nil
}
// Diff ...
func (bx *Box) Diff([]string) error {
return fmt.Errorf("NOT IMPLEMENTED: Diff")
}
// Edit unencrypts, calls editor, calls encrypt.
func (bx *Box) Edit(names []string) error {
if err := anyGpg(names); err != nil {
return err
}
err := bx.getFiles()
if err != nil {
return err
}
for _, name := range names {
if _, ok := bx.FilesSet[name]; ok {
if !bbutil.FileExistsOrProblem(name) {
err := bx.Crypter.Decrypt(name, bx.Umask, false)
if err != nil {
return fmt.Errorf("edit failed %q: %w", name, err)
}
}
}
err := bbutil.RunBash(bx.Editor, name)
if err != nil {
return err
}
}
return nil
}
// Encrypt encrypts a file.
func (bx *Box) Encrypt(names []string, shred bool) error {
var err error
if err = anyGpg(names); err != nil {
return err
}
err = bx.getAdmins()
if err != nil {
return err
}
err = bx.getFiles()
if err != nil {
return err
}
if len(names) == 0 {
names = bx.Files
}
enames, err := encryptMany(bx, names, shred)
bx.Vcs.NeedsCommit(
PrettyCommitMessage("ENCRYPTED", names),
bx.RepoBaseDir,
enames,
)
return err
}
func encryptMany(bx *Box, names []string, shred bool) ([]string, error) {
var enames []string
for _, name := range names {
fmt.Printf("========== ENCRYPTING %q\n", name)
if !bx.FilesSet[name] {
bx.logErr.Printf("Skipping %q: File not registered with Blackbox", name)
continue
}
if !bbutil.FileExistsOrProblem(name) {
bx.logErr.Printf("Skipping. Plaintext does not exist: %q", name)
continue
}
ename, err := bx.Crypter.Encrypt(name, bx.Umask, bx.Admins)
if err != nil {
bx.logErr.Printf("Failed to encrypt %q: %v", name, err)
continue
}
enames = append(enames, ename)
if shred {
bx.Shred([]string{name})
}
}
return enames, nil
}
// FileAdd enrolls files.
func (bx *Box) FileAdd(names []string, shred bool) error {
bx.logDebug.Printf("FileAdd(shred=%v, %v)", shred, names)
// Check for dups.
// Encrypt them all.
// If that succeeds, add to the blackbox-files.txt file.
// (optionally) shred the plaintext.
// FIXME(tlim): Check if the plaintext is in GIT. If it is,
// remove it from Git and print a warning that they should
// eliminate the history or rotate any secrets.
if err := anyGpg(names); err != nil {
return err
}
err := bx.getAdmins()
if err != nil {
return err
}
err = bx.getFiles()
if err != nil {
return err
}
if err := anyGpg(names); err != nil {
return err
}
// Check for newlines
for _, n := range names {
if strings.ContainsAny(n, "\n") {
return fmt.Errorf("file %q contains a newlineregistered", n)
}
}
// Check for duplicates.
for _, n := range names {
if i := sort.SearchStrings(bx.Files, n); i < len(bx.Files) && bx.Files[i] == n {
return fmt.Errorf("file %q already registered", n)
}
}
// Encrypt
var needsCommit []string
for _, name := range names {
s, err := bx.Crypter.Encrypt(name, bx.Umask, bx.Admins)
if err != nil {
return fmt.Errorf("AdminAdd failed AddNewKey: %v", err)
}
needsCommit = append(needsCommit, s)
}
// TODO(tlim): Try the json file.
// Try the legacy file:
fn := filepath.Join(bx.ConfigPath, "blackbox-files.txt")
bx.logDebug.Printf("Files file: %q", fn)
err = bbutil.AddLinesToSortedFile(fn, names...)
if err != nil {
return fmt.Errorf("could not update file (%q,%q): %v", fn, names, err)
}
err = bx.Shred(names)
if err != nil {
bx.logErr.Printf("Error while shredding: %v", err)
}
bx.Vcs.CommitTitle("BLACKBOX ADD FILE: " + makesafe.FirstFew(makesafe.ShellMany(names)))
bx.Vcs.IgnoreFiles(bx.RepoBaseDir, names)
bx.Vcs.NeedsCommit(
PrettyCommitMessage("blackbox-files.txt add", names),
bx.RepoBaseDir,
append([]string{filepath.Join(bx.ConfigPath, "blackbox-files.txt")}, needsCommit...),
)
return nil
}
// FileList lists the files.
func (bx *Box) FileList() error {
err := bx.getFiles()
if err != nil {
return err
}
for _, v := range bx.Files {
fmt.Println(v)
}
return nil
}
// FileRemove de-enrolls files.
func (bx *Box) FileRemove(names []string) error {
return fmt.Errorf("NOT IMPLEMENTED: FileRemove")
}
// Info prints debugging info.
func (bx *Box) Info() error {
err := bx.getFiles()
if err != nil {
bx.logErr.Printf("Info getFiles: %v", err)
}
err = bx.getAdmins()
if err != nil {
bx.logErr.Printf("Info getAdmins: %v", err)
}
fmt.Println("BLACKBOX:")
fmt.Printf(" Debug: %v\n", bx.Debug)
fmt.Printf(" Team: %q\n", bx.Team)
fmt.Printf(" RepoBaseDir: %q\n", bx.RepoBaseDir)
fmt.Printf(" ConfigPath: %q\n", bx.ConfigPath)
fmt.Printf(" Umask: %04o\n", bx.Umask)
fmt.Printf(" Editor: %v\n", bx.Editor)
fmt.Printf(" Shredder: %v\n", bbutil.ShredInfo())
fmt.Printf(" Admins: count=%v\n", len(bx.Admins))
fmt.Printf(" Files: count=%v\n", len(bx.Files))
fmt.Printf(" FilesSet: count=%v\n", len(bx.FilesSet))
fmt.Printf(" Vcs: %v\n", bx.Vcs)
fmt.Printf(" VcsName: %q\n", bx.Vcs.Name())
fmt.Printf(" Crypter: %v\n", bx.Crypter)
fmt.Printf(" CrypterName: %q\n", bx.Crypter.Name())
return nil
}
// Init initializes a repo.
func (bx *Box) Init(yes, vcsname string) error {
fmt.Printf("VCS root is: %q\n", bx.RepoBaseDir)
fmt.Printf("team is: %q\n", bx.Team)
fmt.Printf("configdir will be: %q\n", bx.ConfigPath)
if yes != "yes" {
fmt.Printf("Enable blackbox for this %v repo? (yes/no)? ", bx.Vcs.Name())
input := bufio.NewScanner(os.Stdin)
input.Scan()
ans := input.Text()
b, err := strconv.ParseBool(ans)
if err != nil {
b = false
if len(ans) > 0 {
if ans[0] == 'y' || ans[0] == 'Y' {
b = true
}
}
}
if !b {
fmt.Println("Ok. Maybe some other time.")
return nil
}
}
err := os.Mkdir(bx.ConfigPath, 0o750)
if err != nil {
return err
}
ba := filepath.Join(bx.ConfigPath, "blackbox-admins.txt")
bf := filepath.Join(bx.ConfigPath, "blackbox-files.txt")
bbutil.Touch(ba)
bbutil.Touch(bf)
bx.Vcs.SetFileTypeUnix(bx.RepoBaseDir, ba, bf)
bx.Vcs.IgnoreAnywhere(bx.RepoBaseDir, []string{
"pubring.gpg~",
"pubring.kbx~",
"secring.gpg",
})
fs := []string{ba, bf}
bx.Vcs.NeedsCommit(
"NEW: "+strings.Join(makesafe.RedactMany(fs), " "),
bx.RepoBaseDir,
fs,
)
bx.Vcs.CommitTitle("INITIALIZE BLACKBOX")
return nil
}
// Reencrypt decrypts and reencrypts files.
func (bx *Box) Reencrypt(names []string, overwrite bool, bulkpause bool) error {
allFiles := false
if err := anyGpg(names); err != nil {
return err
}
if err := bx.getAdmins(); err != nil {
return err
}
if err := bx.getFiles(); err != nil {
return err
}
if len(names) == 0 {
names = bx.Files
allFiles = true
}
if bulkpause {
gpgAgentNotice()
}
fmt.Println("========== blackbox administrators are:")
bx.AdminList()
fmt.Println("========== (the above people will be able to access the file)")
if overwrite {
bbutil.ShredFiles(names)
} else {
warned := false
for _, n := range names {
if bbutil.FileExistsOrProblem(n) {
if !warned {
fmt.Printf("========== Shred these files?\n")
warned = true
}
fmt.Println("SHRED?", n)
}
}
if warned {
shouldWeOverwrite()
}
}
// Decrypt
if err := decryptMany(bx, names, overwrite, false, 0); err != nil {
return fmt.Errorf("reencrypt failed decrypt: %w", err)
}
enames, err := encryptMany(bx, names, false)
if err != nil {
return fmt.Errorf("reencrypt failed encrypt: %w", err)
}
if err := bbutil.ShredFiles(names); err != nil {
return fmt.Errorf("reencrypt failed shred: %w", err)
}
if allFiles {
// If the "--all" flag was used, don't try to list all the files.
bx.Vcs.NeedsCommit(
"REENCRYPT all files",
bx.RepoBaseDir,
enames,
)
} else {
bx.Vcs.NeedsCommit(
PrettyCommitMessage("REENCRYPT", names),
bx.RepoBaseDir,
enames,
)
}
return nil
}
// Shred shreds files.
func (bx *Box) Shred(names []string) error {
if err := anyGpg(names); err != nil {
return err
}
err := bx.getFiles()
// Calling getFiles() has the benefit of making sure we are in a repo.
if err != nil {
return err
}
if len(names) == 0 {
names = bx.Files
}
return bbutil.ShredFiles(names)
}
// Status prints the status of files.
func (bx *Box) Status(names []string, nameOnly bool, match string) error {
err := bx.getFiles()
if err != nil {
return err
}
var flist []string
if len(names) == 0 {
flist = bx.Files
} else {
flist = names
}
var data [][]string
var onlylist []string
thirdColumn := false
var tcData bool
for _, name := range flist {
var stat string
var err error
if _, ok := bx.FilesSet[name]; ok {
stat, err = FileStatus(name)
} else {
stat, err = "NOTREG", nil
}
if (match == "") || (stat == match) {
if err == nil {
data = append(data, []string{stat, name})
onlylist = append(onlylist, name)
} else {
thirdColumn = tcData
data = append(data, []string{stat, name, fmt.Sprintf("%v", err)})
onlylist = append(onlylist, fmt.Sprintf("%v: %v", name, err))
}
}
}
if nameOnly {
fmt.Println(strings.Join(onlylist, "\n"))
return nil
}
table := tablewriter.NewWriter(os.Stdout)
table.SetAutoWrapText(false)
if thirdColumn {
table.SetHeader([]string{"Status", "Name", "Error"})
} else {
table.SetHeader([]string{"Status", "Name"})
}
for _, v := range data {
table.Append(v)
}
table.Render() // Send output
return nil
}
// TestingInitRepo initializes a repo.
// Uses bx.Vcs to create ".git" or whatever.
// Uses bx.Vcs to discover what was created, testing its work.
func (bx *Box) TestingInitRepo() error {
if bx.Vcs == nil {
fmt.Println("bx.Vcs is nil")
fmt.Printf("BLACKBOX_VCS=%q\n", os.Getenv("BLACKBOX_VCS"))
os.Exit(1)
}
fmt.Printf("ABOUT TO CALL TestingInitRepo\n")
fmt.Printf("vcs = %v\n", bx.Vcs.Name())
err := bx.Vcs.TestingInitRepo()
fmt.Printf("RETURNED from TestingInitRepo: %v\n", err)
fmt.Println(os.Getwd())
if err != nil {
return fmt.Errorf("TestingInitRepo returned: %w", err)
}
if b, _ := bx.Vcs.Discover(); !b {
return fmt.Errorf("TestingInitRepo failed Discovery")
}
return nil
}