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

22
cmd/blackbox/blackbox.go Normal file
View File

@@ -0,0 +1,22 @@
package main
import (
"fmt"
"os"
_ "github.com/StackExchange/blackbox/v2/pkg/crypters"
_ "github.com/StackExchange/blackbox/v2/pkg/crypters/_all"
_ "github.com/StackExchange/blackbox/v2/pkg/vcs"
_ "github.com/StackExchange/blackbox/v2/pkg/vcs/_all"
)
var dryRun bool
func main() {
app := flags()
err := app.Run(os.Args)
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: %s\n", err)
os.Exit(1)
}
}

226
cmd/blackbox/cli.go Normal file
View File

@@ -0,0 +1,226 @@
package main
// cli.go -- Create urfave/cli datastructures and apply them.
import (
"fmt"
"syscall"
"github.com/urfave/cli/v2"
)
func flags() *cli.App {
app := cli.NewApp()
app.Version = "2.0.0"
app.Usage = "Maintain encrypted files in a VCS (Git, Hg, Svn)"
defUmask := syscall.Umask(0)
syscall.Umask(defUmask)
defUmaskS := fmt.Sprintf("%04o", defUmask)
app.Flags = []cli.Flag{
// &cli.BoolFlag{
// Name: "dry-run",
// Aliases: []string{"n"},
// Usage: "show what would have been done",
// Destination: &dryRun,
// },
&cli.StringFlag{
Name: "vcs",
Usage: "Use this VCS (GIT, NONE) rather than autodetect",
EnvVars: []string{"BLACKBOX_VCS"},
},
&cli.StringFlag{
Name: "crypto",
Usage: "Crypto back-end plugin",
Value: "GnuPG",
EnvVars: []string{"BLACKBOX_CRYPTO"},
},
&cli.StringFlag{
Name: "config",
Usage: "Path to config",
//Value: ".blackbox",
EnvVars: []string{"BLACKBOX_CONFIGDIR", "BLACKBOXDATA"},
},
&cli.StringFlag{
Name: "team",
Usage: "Use .blackbox-$TEAM as the configdir",
EnvVars: []string{"BLACKBOX_TEAM"},
},
&cli.StringFlag{
Name: "editor",
Usage: "editor to use",
Value: "vi",
EnvVars: []string{"EDITOR", "BLACKBOX_EDITOR"},
},
&cli.StringFlag{
Name: "umask",
Usage: "umask to set when decrypting",
Value: defUmaskS,
EnvVars: []string{"BLACKBOX_UMASK", "DECRYPT_UMASK"},
},
&cli.BoolFlag{
Name: "debug",
Usage: "Show debug output",
EnvVars: []string{"BLACKBOX_DEBUG"},
},
}
app.Commands = []*cli.Command{
// List items in the order they appear in the help menu.
{
Name: "decrypt",
Aliases: []string{"de", "start"},
Usage: "Decrypt file(s)",
Flags: []cli.Flag{
&cli.BoolFlag{Name: "all", Usage: "All registered files"},
&cli.BoolFlag{Name: "agentcheck", Usage: "Do not check for gpg-agent when using --all"},
&cli.StringFlag{Name: "group", Usage: "Set group ownership"},
&cli.BoolFlag{Name: "overwrite", Usage: "Overwrite plaintext if it exists"},
},
Action: func(c *cli.Context) error { return cmdDecrypt(c) },
},
{
Name: "encrypt",
Aliases: []string{"en", "end"},
Usage: "Encrypts file(s)",
Flags: []cli.Flag{
&cli.BoolFlag{Name: "shred", Usage: "Remove plaintext afterwords"},
},
Action: func(c *cli.Context) error { return cmdEncrypt(c) },
},
{
Name: "edit",
Aliases: []string{"vi"},
Usage: "Runs $EDITOR on file(s) (decrypt if needed)",
Action: func(c *cli.Context) error { return cmdEdit(c) },
},
{
Name: "cat",
Usage: "Output plaintext to stderr (decrypt if needed)",
Action: func(c *cli.Context) error { return cmdCat(c) },
},
{
Name: "diff",
Usage: "Diffs against encrypted version",
Flags: []cli.Flag{
&cli.BoolFlag{Name: "all", Usage: "all files"},
},
Action: func(c *cli.Context) error { return cmdDiff(c) },
},
{
Name: "init",
Category: "ADMINISTRATIVE",
Usage: "Initialized blackbox for this repo",
Action: func(c *cli.Context) error { return cmdInit(c) },
},
{
Name: "admin",
Category: "ADMINISTRATIVE",
Usage: "Add/list/remove administrators",
Subcommands: []*cli.Command{
{
Name: "add",
Usage: "Adds admin(s)",
Action: func(c *cli.Context) error { return cmdAdminAdd(c) },
},
{
Name: "list",
Usage: "Lists admins",
Action: func(c *cli.Context) error { return cmdAdminList(c) },
},
{
Name: "remove",
Usage: "Remove admin(s)",
Action: func(c *cli.Context) error { return cmdAdminRemove(c) },
},
},
},
{
Name: "file",
Category: "ADMINISTRATIVE",
Usage: "Add/list/remove files from the registry",
Subcommands: []*cli.Command{
{
Name: "add",
Usage: "Registers file with the system",
Flags: []cli.Flag{
&cli.BoolFlag{Name: "shred", Usage: "Remove plaintext afterwords"},
},
Action: func(c *cli.Context) error { return cmdFileAdd(c) },
},
{
Name: "list",
Usage: "Lists the registered files",
Action: func(c *cli.Context) error { return cmdFileList(c) },
},
{
Name: "remove",
Usage: "Deregister file from the system",
Action: func(c *cli.Context) error { return cmdFileRemove(c) },
},
},
},
{
Name: "info",
Category: "DEBUG",
Usage: "Report what we know about this repo",
Action: func(c *cli.Context) error { return cmdInfo(c) },
},
{
Name: "shred",
Usage: "Shred files, or --all for all registered files",
Flags: []cli.Flag{
&cli.BoolFlag{Name: "all", Usage: "All registered files"},
},
Action: func(c *cli.Context) error { return cmdShred(c) },
},
{
Name: "status",
Category: "ADMINISTRATIVE",
Usage: "Print status of files",
Flags: []cli.Flag{
&cli.BoolFlag{Name: "name-only", Usage: "Show only names of the files"},
&cli.BoolFlag{Name: "all", Usage: "All registered files"},
&cli.StringFlag{Name: "type", Usage: "only list if status matching this string"},
},
Action: func(c *cli.Context) error { return cmdStatus(c) },
},
{
Name: "reencrypt",
Usage: "Decrypt then re-encrypt files (erases any plaintext)",
Category: "ADMINISTRATIVE",
Flags: []cli.Flag{
&cli.BoolFlag{Name: "all", Usage: "All registered files"},
&cli.BoolFlag{Name: "overwrite", Usage: "Overwrite plaintext if it exists"},
&cli.BoolFlag{Name: "agentcheck", Usage: "Do not check for gpg-agent when using --all"},
},
Action: func(c *cli.Context) error { return cmdReencrypt(c) },
},
{
Name: "testing_init",
Usage: "For use with integration test",
Category: "INTEGRATION TEST",
Action: func(c *cli.Context) error { return testingInit(c) },
},
//
}
return app
}

296
cmd/blackbox/drive.go Normal file
View File

@@ -0,0 +1,296 @@
package main
// Now that cli.go has processed the flags, validate there are no
// conflicts and drive to the business logic.
import (
"fmt"
"log"
"os"
"github.com/StackExchange/blackbox/v2/pkg/bblog"
"github.com/StackExchange/blackbox/v2/pkg/box"
"github.com/urfave/cli/v2"
)
var logErr *log.Logger
func init() {
if logErr == nil {
logErr = log.New(os.Stderr, "", 0)
}
}
func allOrSomeFiles(c *cli.Context) error {
if c.Bool("all") && c.Args().Present() {
return fmt.Errorf("Can not specify filenames and --all")
}
if (!c.Args().Present()) && (!c.Bool("all")) {
return fmt.Errorf("Must specify at least one file name or --all")
}
return nil
}
const roError = `This command is disabled due to --config flag being used.
We can not determine if the flag's value is in or out of the repo, and
Blackbox can only work on one repo at a time. If the value is inside the
repo, and you'd like to suggest an algorithm that would let us determine that
automatically, please file a bug. We'd love to have this work better. In the
meanwhile, run this command without the --config flag, perhaps after cd'ing
to the base of the repo.`
// Keep these functions in alphabetical order.
func cmdAdminAdd(c *cli.Context) error {
if c.NArg() == 0 || c.NArg() > 2 {
return fmt.Errorf(
"Must specify one admin's GnuPG user-id (i.e. email address) and optionally the directory of the pubkey data (default ~/.GnuPG)")
}
bx := box.NewFromFlags(c)
if bx.ConfigRO {
return fmt.Errorf(roError)
}
err := bx.AdminAdd(c.Args().Get(0), c.Args().Get(1))
if err != nil {
return err
}
return bx.Vcs.FlushCommits()
}
func cmdAdminList(c *cli.Context) error {
if c.Args().Present() {
return fmt.Errorf("This command takes zero arguments")
}
bx := box.NewFromFlags(c)
err := bx.AdminList()
if err != nil {
return err
}
return bx.Vcs.FlushCommits()
}
func cmdAdminRemove(c *cli.Context) error {
if !c.Args().Present() {
return fmt.Errorf("Must specify at least one admin's GnuPG user-id (i.e. email address)")
}
bx := box.NewFromFlags(c)
if bx.ConfigRO {
return fmt.Errorf(roError)
}
err := bx.AdminRemove(c.Args().Slice())
if err != nil {
return err
}
return bx.Vcs.FlushCommits()
}
func cmdCat(c *cli.Context) error {
if !c.Args().Present() {
return fmt.Errorf("Must specify at least one file name")
}
bx := box.NewFromFlags(c)
err := bx.Cat(c.Args().Slice())
if err != nil {
return err
}
return bx.Vcs.FlushCommits()
}
func cmdDecrypt(c *cli.Context) error {
if err := allOrSomeFiles(c); err != nil {
return err
}
// The default for --agentcheck is off normally, and on when using --all.
pauseNeeded := c.Bool("all")
// If the user used the flag, abide by it.
if c.IsSet("agentcheck") {
pauseNeeded = c.Bool("agentcheck")
}
bx := box.NewFromFlags(c)
err := bx.Decrypt(c.Args().Slice(),
c.Bool("overwrite"),
pauseNeeded,
c.String("group"),
)
if err != nil {
return err
}
return bx.Vcs.FlushCommits()
}
func cmdDiff(c *cli.Context) error {
if err := allOrSomeFiles(c); err != nil {
return err
}
bx := box.NewFromFlags(c)
err := bx.Diff(c.Args().Slice())
if err != nil {
return err
}
return bx.Vcs.FlushCommits()
}
func cmdEdit(c *cli.Context) error {
if !c.Args().Present() {
return fmt.Errorf("Must specify at least one file name")
}
bx := box.NewFromFlags(c)
err := bx.Edit(c.Args().Slice())
if err != nil {
return err
}
return bx.Vcs.FlushCommits()
}
func cmdEncrypt(c *cli.Context) error {
if err := allOrSomeFiles(c); err != nil {
return err
}
bx := box.NewFromFlags(c)
err := bx.Encrypt(c.Args().Slice(), c.Bool("shred"))
if err != nil {
return err
}
return bx.Vcs.FlushCommits()
}
func cmdFileAdd(c *cli.Context) error {
if !c.Args().Present() {
return fmt.Errorf("Must specify at least one file name")
}
bx := box.NewFromFlags(c)
if bx.ConfigRO {
return fmt.Errorf(roError)
}
err := bx.FileAdd(c.Args().Slice(), c.Bool("shred"))
if err != nil {
return err
}
return bx.Vcs.FlushCommits()
}
func cmdFileList(c *cli.Context) error {
if c.Args().Present() {
return fmt.Errorf("This command takes zero arguments")
}
bx := box.NewFromFlags(c)
err := bx.FileList()
if err != nil {
return err
}
return bx.Vcs.FlushCommits()
}
func cmdFileRemove(c *cli.Context) error {
if !c.Args().Present() {
return fmt.Errorf("Must specify at least one file name")
}
bx := box.NewFromFlags(c)
if bx.ConfigRO {
return fmt.Errorf(roError)
}
err := bx.FileRemove(c.Args().Slice())
if err != nil {
return err
}
return bx.Vcs.FlushCommits()
}
func cmdInfo(c *cli.Context) error {
if c.Args().Present() {
return fmt.Errorf("This command takes zero arguments")
}
bx := box.NewFromFlags(c)
err := bx.Info()
if err != nil {
return err
}
return bx.Vcs.FlushCommits()
}
func cmdInit(c *cli.Context) error {
if c.Args().Len() > 1 {
return fmt.Errorf("This command takes one or two arguments")
}
bx := box.NewUninitialized(c)
if bx.ConfigRO {
return fmt.Errorf(roError)
}
err := bx.Init(c.Args().First(), c.String("vcs"))
if err != nil {
return err
}
return bx.Vcs.FlushCommits()
}
func cmdReencrypt(c *cli.Context) error {
if err := allOrSomeFiles(c); err != nil {
return err
}
// The default for --agentcheck is off normally, and on when using --all.
pauseNeeded := c.Bool("all")
// If the user used the flag, abide by it.
if c.IsSet("agentcheck") {
pauseNeeded = c.Bool("agentcheck")
}
bx := box.NewFromFlags(c)
err := bx.Reencrypt(c.Args().Slice(),
c.Bool("overwrite"),
pauseNeeded,
)
if err != nil {
return err
}
return bx.Vcs.FlushCommits()
}
func cmdShred(c *cli.Context) error {
if err := allOrSomeFiles(c); err != nil {
return err
}
bx := box.NewFromFlags(c)
err := bx.Shred(c.Args().Slice())
if err != nil {
return err
}
return bx.Vcs.FlushCommits()
}
func cmdStatus(c *cli.Context) error {
if c.Bool("all") && c.Args().Present() {
return fmt.Errorf("Can not specify filenames and --all")
}
bx := box.NewFromFlags(c)
err := bx.Status(c.Args().Slice(), c.Bool("name-only"), c.String("type"))
if err != nil {
return err
}
return bx.Vcs.FlushCommits()
}
// These are "secret" commands used by the integration tests.
func testingInit(c *cli.Context) error {
if c.Args().Present() {
return fmt.Errorf("No args required")
}
logDebug := bblog.GetDebug(c.Bool("debug"))
logDebug.Printf(
"c.String(vcs) reports %q\n",
c.String("vcs"),
)
bx := box.NewForTestingInit(c.String("vcs"))
if bx.ConfigRO {
return fmt.Errorf(roError)
}
err := bx.TestingInitRepo()
if err != nil {
return err
}
return bx.Vcs.FlushCommits()
}