233
pkg/box/box.go
Normal file
233
pkg/box/box.go
Normal 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
224
pkg/box/boxutils.go
Normal 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
35
pkg/box/pretty_test.go
Normal 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
633
pkg/box/verbs.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user